koishi-plugin-image-prompt 2.0.1 → 2.0.3
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 +7 -1
- package/lib/index.js +98 -58
- package/package.json +28 -28
- package/readme.md +50 -12
- package/src/index.ts +123 -65
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
|
/**
|
|
@@ -216,7 +222,7 @@ interface CommandConfig {
|
|
|
216
222
|
showPrompt?: boolean;
|
|
217
223
|
promptMaxLength?: number;
|
|
218
224
|
appendUserInput?: boolean;
|
|
219
|
-
promptOptimize?: 'off' | 'merge' | 'rewrite';
|
|
225
|
+
promptOptimize?: 'off' | 'passthrough' | 'merge' | 'rewrite';
|
|
220
226
|
optimizePrompt?: string;
|
|
221
227
|
resultGallery?: ResultGalleryConfig;
|
|
222
228
|
backgroundDrawing?: BackgroundDrawingConfig;
|
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();
|
|
@@ -258,7 +270,8 @@ function buildAgentTools() {
|
|
|
258
270
|
parameters: {
|
|
259
271
|
type: "object",
|
|
260
272
|
properties: {
|
|
261
|
-
prompt
|
|
273
|
+
// 提示词要短:宁可朴素,也不要堆术语写成几百字的「专业」prompt
|
|
274
|
+
prompt: { type: "string", description: "\u7ED8\u56FE\u63D0\u793A\u8BCD\u3002\u8BF4\u4EBA\u8BDD\u3001\u5199\u7B80\u77ED\u5C31\u884C\uFF0C\u4E2D\u6587\u5373\u53EF\uFF08\u7ED8\u56FE\u6A21\u578B\u542C\u5F97\u61C2\u4E2D\u6587\uFF09\uFF1B\u4E0D\u8981\u628A\u7528\u6237\u7684\u8BDD\u6269\u5199\u6210\u51E0\u767E\u5B57\uFF0C\u4E5F\u4E0D\u8981\u5806\u780C\u98CE\u683C/\u6750\u8D28/\u5149\u7EBF\u672F\u8BED\u3002\u753B\u9762\u4E0A\u5FC5\u987B\u51FA\u73B0\u7684\u6587\u5B57\u8981\u539F\u6837\u5199\u8FDB\u53BB" },
|
|
262
275
|
references: {
|
|
263
276
|
type: "array",
|
|
264
277
|
items: { type: "string" },
|
|
@@ -745,13 +758,15 @@ function extractTextToRender(text, options = {}) {
|
|
|
745
758
|
}
|
|
746
759
|
var DEFAULT_OPTIMIZE_PROMPT = `\u4F60\u662F\u7ED8\u56FE\u63D0\u793A\u8BCD\u4F18\u5316\u52A9\u624B\u3002\u4E0B\u9762\u662F\u300C\u7ED8\u56FE\u6307\u4EE4\u7684\u539F\u59CB\u63D0\u793A\u8BCD\u300D\u548C\u300C\u7528\u6237\u672C\u6B21\u7684\u9700\u6C42\u300D\u3002
|
|
747
760
|
|
|
748
|
-
\u8BF7\u8F93\u51FA\u4E00\u4EFD\
|
|
761
|
+
\u8BF7\u8F93\u51FA\u4E00\u4EFD**\u7B80\u77ED**\u7684\u3001\u53EF\u76F4\u63A5\u7528\u4E8E\u7ED8\u56FE\u6A21\u578B\u7684\u63D0\u793A\u8BCD\u3002\u89C4\u5219\uFF1A
|
|
749
762
|
|
|
750
|
-
1. \u539F\u59CB\u63D0\u793A\u8BCD\u4E0D\u4E3A\u7A7A\u65F6\uFF1A\u628A\u7528\u6237\u9700\u6C42\u81EA\u7136\u5730\u878D\
|
|
751
|
-
2. \u539F\u59CB\u63D0\u793A\u8BCD\u4E3A\u7A7A\u65F6\
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
763
|
+
1. \u539F\u59CB\u63D0\u793A\u8BCD\u4E0D\u4E3A\u7A7A\u65F6\uFF1A\u628A\u7528\u6237\u9700\u6C42\u81EA\u7136\u5730\u878D\u5165\u539F\u59CB\u63D0\u793A\u8BCD\uFF0C\u4FDD\u7559\u5176\u4E2D\u7684\u6784\u56FE\u4E0E\u98CE\u683C\uFF0C\u4E0D\u8981\u5220\u51CF\u3002
|
|
764
|
+
2. \u539F\u59CB\u63D0\u793A\u8BCD\u4E3A\u7A7A\u65F6\uFF1A\u76F4\u63A5\u6574\u7406\u7528\u6237\u7684\u8BDD\u4F5C\u4E3A\u63D0\u793A\u8BCD\uFF0C**\u4E0D\u8981\u66FF\u4ED6\u8111\u8865**\u753B\u98CE\u3001\u955C\u5934\u3001\u5149\u7EBF\u3001\u6C1B\u56F4\u3001\u914D\u8272\u4E4B\u7C7B\u7684\u7EC6\u8282 \u2014\u2014
|
|
765
|
+
\u7528\u6237\u6CA1\u63D0\u7684\u5C31\u522B\u5199\u3002
|
|
766
|
+
3. \u628A\u7528\u6237\u9700\u6C42\u5199\u5230\u5B83\u8BE5\u53BB\u7684\u4F4D\u7F6E\uFF0C\u4E0D\u8981\u539F\u6837\u8D34\u5728\u672B\u5C3E\uFF0C\u4E5F\u4E0D\u8981\u53E6\u8D77\u4E00\u6BB5\u590D\u8FF0\u4E00\u904D\u3002
|
|
767
|
+
4. **\u8BED\u8A00\u8DDF\u7740\u7528\u6237\u8D70**\uFF1A\u7528\u6237\u7528\u4E2D\u6587\u5C31\u8F93\u51FA\u4E2D\u6587\uFF08\u7ED8\u56FE\u6A21\u578B\u542C\u5F97\u61C2\u4E2D\u6587\uFF0C\u7406\u89E3\u529B\u4E5F\u591F\uFF09\uFF0C
|
|
768
|
+
\u4E0D\u8981\u4E3A\u4E86\u663E\u5F97\u4E13\u4E1A\u800C\u7FFB\u6210\u82F1\u6587\u3001\u4E0D\u8981\u5806\u780C\u672F\u8BED\u3002
|
|
769
|
+
5. **\u5B81\u77ED\u52FF\u957F**\uFF1A\u53EA\u5199\u771F\u6B63\u8981\u5728\u753B\u9762\u4E0A\u51FA\u73B0\u7684\u4E1C\u897F\u3002\u63D0\u793A\u8BCD\u8D8A\u957F\u5404\u8981\u7D20\u4E92\u76F8\u7A00\u91CA\uFF0C\u51FA\u56FE\u53CD\u800C\u66F4\u7CCA\u3002
|
|
755
770
|
|
|
756
771
|
\u539F\u59CB\u63D0\u793A\u8BCD\uFF1A
|
|
757
772
|
{prompt}
|
|
@@ -786,6 +801,11 @@ var DEFAULT_AGENT_INSTRUCTIONS = `\u4F60\u662F QQ \u7FA4\u91CC\u7684\u7ED8\u56FE
|
|
|
786
801
|
6. \u6D41\u7A0B\u7ED3\u675F\u540E\u7528\u4E2D\u6587\u56DE\u4E00\u53E5\u7B80\u77ED\u7684\u7ED3\u679C\u8BF4\u660E\uFF08\u4E0D\u8981\u590D\u8FF0 prompt\uFF0C\u4E0D\u8981\u5199\u5C0F\u4F5C\u6587\uFF0C\u4E0D\u8981\u518D\u95EE\u300C\u8FD8\u8981\u6211\u505A\u4EC0\u4E48\u300D\uFF09\u3002
|
|
787
802
|
7. \u56FE\u7247\u7F16\u53F7\uFF1Agallery_search \u7ED3\u679C\u91CC\u7684 id\u3001\u7528\u6237\u53D1\u7684\u56FE\uFF08user1\u3001user2\u2026\uFF09\u3001\u6307\u4EE4\u9ED8\u8BA4\u56FE\uFF08cmd1\u2026\uFF09\u90FD\u53EF\u4EE5\u76F4\u63A5\u5199\u8FDB draw \u7684 references\u3002
|
|
788
803
|
\u53EA\u586B\u4F60\u786E\u8BA4\u5B58\u5728\u7684\u7F16\u53F7\uFF0C**\u7EDD\u5BF9\u4E0D\u8981\u7F16\u9020 id**\u3002
|
|
804
|
+
8. \u3010prompt \u8981\u77ED\uFF0C\u8981\u8BF4\u4EBA\u8BDD\u3011**\u4E0D\u8981**\u628A\u7528\u6237\u7684\u8BDD\u6269\u5199\u6210\u51E0\u767E\u5B57\u7684\u300C\u4E13\u4E1A\u300D\u63D0\u793A\u8BCD\uFF1A
|
|
805
|
+
- \u7528\u6237\u7528\u4E2D\u6587\u63D0\u7684\u9700\u6C42\uFF0Cprompt \u5C31\u5199\u4E2D\u6587\uFF08\u7ED8\u56FE\u6A21\u578B\u542C\u5F97\u61C2\u4E2D\u6587\uFF0C\u7406\u89E3\u529B\u4E5F\u591F\u7528\uFF09\uFF1B
|
|
806
|
+
- \u522B\u4E3A\u4E86\u663E\u5F97\u4E13\u4E1A\u5C31\u53BB\u5806\u98CE\u683C / \u6750\u8D28 / \u5149\u7EBF / \u955C\u5934\u672F\u8BED\uFF0C\u66F4\u522B\u523B\u610F\u7FFB\u6210\u82F1\u6587\uFF1B
|
|
807
|
+
- \u53EA\u4FDD\u7559\u7528\u6237\u771F\u6B63\u8BF4\u8FC7\u7684\u5185\u5BB9 + \u753B\u9762\u5FC5\u987B\u6709\u7684\u8981\u7D20\uFF0C**\u4E0D\u8981\u66FF\u4ED6\u8111\u8865\u7EC6\u8282**\uFF1B
|
|
808
|
+
- \u63D0\u793A\u8BCD\u8D8A\u957F\uFF0C\u5404\u8981\u7D20\u4E92\u76F8\u7A00\u91CA\uFF0C\u51FA\u56FE\u53CD\u800C\u8D8A\u8D70\u6837\u3002\u4E00\u4E24\u53E5\u8BDD\u80FD\u628A\u753B\u9762\u8BF4\u6E05\u5C31\u591F\u4E86\u3002
|
|
789
809
|
|
|
790
810
|
\u5982\u679C\u4F60\u6240\u5728\u7684\u63A5\u53E3\u4E0D\u652F\u6301\u51FD\u6570\u8C03\u7528\uFF0C\u5C31**\u53EA\u8F93\u51FA\u4E00\u884C JSON** \u6765\u8C03\u7528\u5DE5\u5177\uFF0C\u4F8B\u5982\uFF1A
|
|
791
811
|
{"tool":"gallery_search","args":{"keyword":"\u767D\u53D1 \u7ACB\u7ED8"}}
|
|
@@ -1349,10 +1369,11 @@ var Config = import_koishi.Schema.intersect([
|
|
|
1349
1369
|
import_koishi.Schema.object({
|
|
1350
1370
|
appendUserInput: import_koishi.Schema.boolean().default(true).description("\u628A\u7528\u6237\u968F\u6307\u4EE4\u53D1\u7684\u9644\u52A0\u9700\u6C42\u5E76\u5165\u7ED8\u56FE\u63D0\u793A\u8BCD\uFF08\u4F8B\u5982\u300C\u624B\u529E\u5316 xxx \u5728\u5077\u5403\u767D\u996D\u88AB\u53D1\u73B0\u7684\u8868\u60C5\u300D\uFF09"),
|
|
1351
1371
|
promptOptimize: import_koishi.Schema.union([
|
|
1352
|
-
import_koishi.Schema.const("
|
|
1353
|
-
import_koishi.Schema.const("merge").description("\
|
|
1354
|
-
import_koishi.Schema.const("
|
|
1355
|
-
|
|
1372
|
+
import_koishi.Schema.const("passthrough").description("\u539F\u8BDD\u76F4\u51FA\uFF08\u63A8\u8350\uFF09\uFF1A\u7528\u6237\u8BF4\u4E86\u5C31\u7528\u7528\u6237\u7684\u539F\u8BDD\uFF0C\u4E0D\u518D\u62FC\u63A5\u6307\u4EE4\u81EA\u5E26\u7684\u63D0\u793A\u8BCD\uFF0C\u4E5F\u4E0D\u505A\u6269\u5199"),
|
|
1373
|
+
import_koishi.Schema.const("merge").description("\u8FFD\u52A0\uFF1A\u628A\u7528\u6237\u7684\u539F\u8BDD\u8D34\u5728\u6307\u4EE4\u81EA\u5E26\u63D0\u793A\u8BCD\u540E\u9762"),
|
|
1374
|
+
import_koishi.Schema.const("rewrite").description("\u878D\u5408\u91CD\u5199\uFF1A\u8BA9\u6A21\u578B\u628A\u9700\u6C42\u5199\u8FDB\u63D0\u793A\u8BCD\u5BF9\u5E94\u4F4D\u7F6E\uFF08\u4F1A\u53D8\u957F\uFF0C\u8001\u7248\u672C\u7684\u9ED8\u8BA4\u884C\u4E3A\uFF09"),
|
|
1375
|
+
import_koishi.Schema.const("off").description("\u4E0D\u5904\u7406\uFF1A\u53EA\u7528\u6307\u4EE4\u81EA\u5E26\u7684\u63D0\u793A\u8BCD\uFF0C\u5FFD\u7565\u7528\u6237\u8BF4\u7684\u8BDD")
|
|
1376
|
+
]).default("passthrough").description("\u7528\u6237\u9644\u52A0\u9700\u6C42\u5982\u4F55\u5E76\u5165\u63D0\u793A\u8BCD\u3002**\u7ED8\u56FE\u6A21\u578B\u542C\u5F97\u61C2\u4E2D\u6587**\uFF0C\u4E00\u822C\u4E0D\u9700\u8981\u518D\u4F18\u5316/\u7FFB\u8BD1"),
|
|
1356
1377
|
optimizePrompt: import_koishi.Schema.string().role("textarea", { rows: [10, 6] }).default(DEFAULT_OPTIMIZE_PROMPT).description("\u878D\u5408\u91CD\u5199\u7528\u7684\u63D0\u793A\u8BCD\u6A21\u677F\uFF0C\u5360\u4F4D\u7B26\uFF1A{prompt} \u539F\u59CB\u63D0\u793A\u8BCD\u3001{userInput} \u7528\u6237\u9644\u52A0\u9700\u6C42"),
|
|
1357
1378
|
showPrompt: import_koishi.Schema.boolean().default(true).description("\u53D1\u9001\u4F18\u5316\u540E\u7684\u63D0\u793A\u8BCD\uFF08QQ / QQ \u9891\u9053\u7528\u4EE3\u7801\u5757\u5305\u88F9\uFF0C\u5176\u5B83\u5E73\u53F0\u53D1\u7EAF\u6587\u672C\uFF09"),
|
|
1358
1379
|
promptMaxLength: import_koishi.Schema.number().default(4e3).min(0).max(2e4).step(100).description("\u56DE\u663E\u63D0\u793A\u8BCD\u7684\u6700\u5927\u5B57\u7B26\u6570\uFF0C\u8D85\u51FA\u90E8\u5206\u622A\u65AD\u5E76\u6807\u6CE8\uFF1B\u8BBE\u4E3A 0 \u8868\u793A\u4E0D\u622A\u65AD\uFF08QQ markdown \u6709\u957F\u5EA6\u4E0A\u9650\uFF0C\u592A\u957F\u53EF\u80FD\u88AB\u62D2\u6536\uFF09")
|
|
@@ -1771,7 +1792,10 @@ Negative prompt: ${String(args.negative_prompt).trim()}` : prompt;
|
|
|
1771
1792
|
ok: res.ok,
|
|
1772
1793
|
references: capped,
|
|
1773
1794
|
...unknown.length ? { unknown } : {},
|
|
1774
|
-
...res.ok ? { note: "\u56FE\u5DF2\
|
|
1795
|
+
...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" } : {
|
|
1796
|
+
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)}`,
|
|
1797
|
+
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"
|
|
1798
|
+
}
|
|
1775
1799
|
});
|
|
1776
1800
|
}
|
|
1777
1801
|
}
|
|
@@ -1895,17 +1919,24 @@ ${echo}` : echo);
|
|
|
1895
1919
|
done: session.text("image-prompt.messages.bgdone"),
|
|
1896
1920
|
failed: session.text("image-prompt.messages.failed"),
|
|
1897
1921
|
error: session.text("image-prompt.messages.error")
|
|
1922
|
+
},
|
|
1923
|
+
/** 带上接口给的真实原因,用户一看就知道是模型那边的问题还是自己配置的问题 */
|
|
1924
|
+
formatFailed: (reason) => {
|
|
1925
|
+
const head = String(session.text("image-prompt.messages.failed") || "").replace(/[,,。]\s*$/, "");
|
|
1926
|
+
if (!reason) return `${head}\u3002`;
|
|
1927
|
+
const clean = String(reason).replace(/\s+/g, " ").trim();
|
|
1928
|
+
return `${head}\uFF08${clean.slice(0, 120)}\uFF09`;
|
|
1898
1929
|
}
|
|
1899
1930
|
};
|
|
1900
1931
|
const queued = enqueueDrawJob(snapshot, files);
|
|
1901
1932
|
await sendNotice(session, `${session.text("image-prompt.messages.bgstart", [files.length])}${queued > 0 ? session.text("image-prompt.messages.bgqueue", [queued]) : ""}`);
|
|
1902
|
-
return { ok: true };
|
|
1933
|
+
return { ok: true, queued: true };
|
|
1903
1934
|
}
|
|
1904
|
-
const
|
|
1905
|
-
if (!
|
|
1906
|
-
await saveToGallery(
|
|
1907
|
-
await sendResultImage(session,
|
|
1908
|
-
return { ok: true, url:
|
|
1935
|
+
const gen = await generateFigureImage(files, text);
|
|
1936
|
+
if (!gen.url) return { ok: false, error: "failed", reason: gen.reason };
|
|
1937
|
+
await saveToGallery(gen.url, buildGalleryDescription(text, userInputText), commandName);
|
|
1938
|
+
await sendResultImage(session, gen.url, commandName);
|
|
1939
|
+
return { ok: true, url: gen.url };
|
|
1909
1940
|
}
|
|
1910
1941
|
async function sendNotice(session, text) {
|
|
1911
1942
|
const value = String(text ?? "").trim();
|
|
@@ -1915,21 +1946,27 @@ ${echo}` : echo);
|
|
|
1915
1946
|
async function runDirectFlow(session, cmdConfig, userInputText, defaultImageUrls) {
|
|
1916
1947
|
const waitTimeout = cmdConfig.waitTimeout || config.defaultWaitTimeout;
|
|
1917
1948
|
const maxImages = cmdConfig.maxImages || 0;
|
|
1918
|
-
|
|
1949
|
+
const basePrompt = cmdConfig.prompt || "";
|
|
1950
|
+
const input = String(userInputText || "").trim();
|
|
1951
|
+
const mode = config.promptOptimize || "passthrough";
|
|
1952
|
+
const takeInput = input && config.appendUserInput !== false;
|
|
1953
|
+
let promptText = mode === "passthrough" ? (takeInput ? input : "") || basePrompt : basePrompt;
|
|
1919
1954
|
let optimizeFailed = false;
|
|
1920
|
-
if (
|
|
1921
|
-
promptText = mergePrompt(
|
|
1922
|
-
logInfo2(`\u5DF2\
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1955
|
+
if (takeInput && mode === "merge") {
|
|
1956
|
+
promptText = mergePrompt(basePrompt, input, true);
|
|
1957
|
+
logInfo2(`\u5DF2\u628A\u7528\u6237\u539F\u8BDD\u8FFD\u52A0\u5230\u6307\u4EE4\u63D0\u793A\u8BCD\u540E: ${input}`);
|
|
1958
|
+
} else if (takeInput && mode === "rewrite") {
|
|
1959
|
+
const optimized = await optimizePromptText(basePrompt, input);
|
|
1960
|
+
if (optimized) {
|
|
1961
|
+
promptText = optimized;
|
|
1962
|
+
logInfo2("\u63D0\u793A\u8BCD\u5DF2\u878D\u5408\u91CD\u5199");
|
|
1963
|
+
} else {
|
|
1964
|
+
ctx.logger.warn("\u63D0\u793A\u8BCD\u878D\u5408\u91CD\u5199\u5931\u8D25\uFF0C\u6539\u7528\u7528\u6237\u7684\u539F\u8BDD");
|
|
1965
|
+
promptText = mergePrompt(basePrompt, input, true);
|
|
1966
|
+
optimizeFailed = true;
|
|
1932
1967
|
}
|
|
1968
|
+
} else if (takeInput && mode !== "off") {
|
|
1969
|
+
logInfo2(`\u5DF2\u5E76\u5165\u7528\u6237\u539F\u8BDD\uFF08${mode} \u6A21\u5F0F\uFF09: ${input}`);
|
|
1933
1970
|
}
|
|
1934
1971
|
if (cmdConfig.custom && !cmdConfig.prompt && !userInputText) {
|
|
1935
1972
|
const [msgId] = await session.send(session.text("image-prompt.messages.customprompt", [waitTimeout]));
|
|
@@ -1941,7 +1978,7 @@ ${echo}` : echo);
|
|
|
1941
1978
|
}
|
|
1942
1979
|
if (!userPrompt) return session.text("image-prompt.messages.needprompt");
|
|
1943
1980
|
const text = extractTextFromMessage(userPrompt) || String(userPrompt).trim();
|
|
1944
|
-
promptText =
|
|
1981
|
+
promptText = mode === "rewrite" && await optimizePromptText("", text) || text;
|
|
1945
1982
|
}
|
|
1946
1983
|
const images = [...defaultImageUrls || [], ...extractImagesFromSession(session)];
|
|
1947
1984
|
const remaining = Math.max(0, maxImages - images.length);
|
|
@@ -1966,7 +2003,7 @@ ${echo}` : echo);
|
|
|
1966
2003
|
if (images.length === 0) return session.text("image-prompt.messages.needimages");
|
|
1967
2004
|
const res = await drawWithFiles(session, cmdConfig, promptText, images, userInputText, { optimizeFailed });
|
|
1968
2005
|
if (!res.ok) {
|
|
1969
|
-
return res.error === "invalidimage" ? session.text("image-prompt.messages.invalidimage") : session.text("image-prompt.messages.failed");
|
|
2006
|
+
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
2007
|
}
|
|
1971
2008
|
}
|
|
1972
2009
|
let runningJobs = 0;
|
|
@@ -1994,14 +2031,14 @@ ${echo}` : echo);
|
|
|
1994
2031
|
const send = (parts) => sendBotMessage(snapshot.bot, snapshot.channelId, parts, snapshot.guildId, snapshot.messageId);
|
|
1995
2032
|
try {
|
|
1996
2033
|
if (!isActive || !ctx.scope.isActive) return;
|
|
1997
|
-
const
|
|
1998
|
-
if (!
|
|
1999
|
-
await send([snapshot.texts.failed]);
|
|
2034
|
+
const gen = await generateFigureImage(files, snapshot.promptText);
|
|
2035
|
+
if (!gen.url) {
|
|
2036
|
+
await send([snapshot.formatFailed ? snapshot.formatFailed(gen.reason) : snapshot.texts.failed]);
|
|
2000
2037
|
return;
|
|
2001
2038
|
}
|
|
2002
|
-
await saveToGallery(
|
|
2039
|
+
await saveToGallery(gen.url, buildGalleryDescription(snapshot.promptText, ""), snapshot.commandName);
|
|
2003
2040
|
await send([`${snapshot.texts.done}
|
|
2004
|
-
`, await buildResultImagePart(
|
|
2041
|
+
`, await buildResultImagePart(gen.url)]);
|
|
2005
2042
|
} catch (error) {
|
|
2006
2043
|
ctx.logger.error(`[${snapshot.commandName}] \u540E\u53F0\u7ED8\u56FE\u5931\u8D25:`, error);
|
|
2007
2044
|
try {
|
|
@@ -2556,18 +2593,20 @@ ${lines.join("\n")}`]);
|
|
|
2556
2593
|
return await sendChatRequest(requestBody);
|
|
2557
2594
|
} catch (error) {
|
|
2558
2595
|
ctx.logger.error(`\u751F\u6210\u56FE\u7247\u65F6\u53D1\u751F\u9519\u8BEF: ${error}`);
|
|
2559
|
-
return null;
|
|
2596
|
+
return { url: null, reason: String(error?.message || error) };
|
|
2560
2597
|
}
|
|
2561
2598
|
}
|
|
2562
2599
|
async function sendChatRequest(requestBody) {
|
|
2563
2600
|
let retryCount = 0;
|
|
2564
|
-
|
|
2601
|
+
const maxAttempts = (config.maxRetries ?? 0) + 1;
|
|
2602
|
+
let lastReason = "";
|
|
2603
|
+
while (retryCount < maxAttempts) {
|
|
2565
2604
|
if (!isActive || !ctx.scope.isActive) {
|
|
2566
2605
|
ctx.logger.info("\u63D2\u4EF6\u5DF2\u5378\u8F7D\uFF0C\u505C\u6B62\u91CD\u8BD5");
|
|
2567
|
-
return null;
|
|
2606
|
+
return { url: null, reason: lastReason };
|
|
2568
2607
|
}
|
|
2569
2608
|
try {
|
|
2570
|
-
logInfo2(`\u53D1\u9001\u8BF7\u6C42\u5230 ${config.baseUrl}\uFF0C\u7B2C ${retryCount + 1} \u6B21\u5C1D\u8BD5`);
|
|
2609
|
+
logInfo2(`\u53D1\u9001\u8BF7\u6C42\u5230 ${config.baseUrl}\uFF0C\u7B2C ${retryCount + 1}/${maxAttempts} \u6B21\u5C1D\u8BD5`);
|
|
2571
2610
|
const headers = {
|
|
2572
2611
|
"Content-Type": "application/json"
|
|
2573
2612
|
};
|
|
@@ -2583,35 +2622,35 @@ ${lines.join("\n")}`]);
|
|
|
2583
2622
|
if (markdownMatch && markdownMatch[1]) {
|
|
2584
2623
|
const imageUrl = markdownMatch[1];
|
|
2585
2624
|
logInfo2(`\u6210\u529F\u83B7\u53D6\u56FE\u7247URL: ${imageUrl}`);
|
|
2586
|
-
return imageUrl;
|
|
2625
|
+
return { url: imageUrl };
|
|
2587
2626
|
}
|
|
2588
2627
|
}
|
|
2589
2628
|
}
|
|
2590
|
-
|
|
2591
|
-
throw new Error(errorMsg);
|
|
2629
|
+
throw new Error("\u54CD\u5E94\u4E2D\u672A\u627E\u5230\u56FE\u7247URL");
|
|
2592
2630
|
} catch (error) {
|
|
2593
2631
|
retryCount++;
|
|
2594
|
-
const
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2632
|
+
const reason = httpErrorText(error);
|
|
2633
|
+
lastReason = reason;
|
|
2634
|
+
const statusCode = error.response?.status || error.status || error.code || 0;
|
|
2635
|
+
logInfo2(`\u8BF7\u6C42\u5931\u8D25\uFF08\u7B2C ${retryCount}/${maxAttempts} \u6B21\uFF09\uFF1A${reason}`);
|
|
2636
|
+
if (reason.includes("insufficient_quota") || statusCode === 429) {
|
|
2598
2637
|
ctx.logger.error("API \u914D\u989D\u4E0D\u8DB3\uFF0C\u505C\u6B62\u91CD\u8BD5");
|
|
2599
|
-
return null;
|
|
2638
|
+
return { url: null, reason };
|
|
2600
2639
|
}
|
|
2601
|
-
if (retryCount
|
|
2602
|
-
|
|
2603
|
-
|
|
2640
|
+
if (retryCount < maxAttempts) {
|
|
2641
|
+
const delay = computeRetryDelay(statusCode, void 0, config.retryInterval, retryCount - 1);
|
|
2642
|
+
logInfo2(`${delay}ms \u540E\u91CD\u8BD5`);
|
|
2643
|
+
await (0, import_koishi.sleep)(delay);
|
|
2604
2644
|
if (!isActive || !ctx.scope.isActive) {
|
|
2605
2645
|
ctx.logger.info("\u63D2\u4EF6\u5DF2\u5378\u8F7D\uFF0C\u505C\u6B62\u91CD\u8BD5");
|
|
2606
|
-
return null;
|
|
2646
|
+
return { url: null, reason };
|
|
2607
2647
|
}
|
|
2608
2648
|
} else {
|
|
2609
|
-
ctx.logger.error(`\
|
|
2610
|
-
return null;
|
|
2649
|
+
ctx.logger.error(`\u5DF2\u91CD\u8BD5 ${maxAttempts} \u6B21\u4ECD\u5931\u8D25\uFF0C\u6700\u540E\u9519\u8BEF\uFF1A${reason}`);
|
|
2611
2650
|
}
|
|
2612
2651
|
}
|
|
2613
2652
|
}
|
|
2614
|
-
return null;
|
|
2653
|
+
return { url: null, reason: lastReason };
|
|
2615
2654
|
}
|
|
2616
2655
|
function logInfo2(...args) {
|
|
2617
2656
|
if (config.loggerinfo) {
|
|
@@ -2639,6 +2678,7 @@ ${lines.join("\n")}`]);
|
|
|
2639
2678
|
extractServerMessage,
|
|
2640
2679
|
extractTextToRender,
|
|
2641
2680
|
fitImageSize,
|
|
2681
|
+
httpErrorText,
|
|
2642
2682
|
inject,
|
|
2643
2683
|
isPassiveReplyError,
|
|
2644
2684
|
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.3",
|
|
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
|
@@ -161,11 +161,12 @@ bot:画好啦,按 ref1 的立绘做的~
|
|
|
161
161
|
|
|
162
162
|
### 📝 提示词回显
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
处理时会把**最终交给绘图模型的提示词**一起发出来,方便你确认它到底用了什么。
|
|
165
|
+
用默认的「原话直出」时,你看到的基本就是你发的那句话。
|
|
165
166
|
|
|
166
167
|
| 配置项 | 说明 | 默认 |
|
|
167
168
|
|---|---|---|
|
|
168
|
-
|
|
|
169
|
+
| 发送最终提示词 | 关闭则不回显 | 开 |
|
|
169
170
|
| 回显提示词的最大字符数 | 超出截断并标注;**0 = 不截断** | 4000 |
|
|
170
171
|
|
|
171
172
|
- **官方 QQ**(`qq` / `qq-*` / `qqbot`):发 `markdown` **元素**,由适配器走 QQ 的 markdown API,
|
|
@@ -224,28 +225,40 @@ bot:画好啦,按 ref1 的立绘做的~
|
|
|
224
225
|
|
|
225
226
|
这句里的附加需求会**同时**用于两件事:
|
|
226
227
|
|
|
227
|
-
1. 交给 AI
|
|
228
|
-
2.
|
|
228
|
+
1. 交给 AI 参考(agent 模式下用于决策;该搜什么、要不要问用户、用哪张参考图)
|
|
229
|
+
2. **原样进入绘图提示词**——这样出图才会带上「偷吃白饭被发现的表情」。
|
|
230
|
+
默认不再加工:不扩写、不翻译成英文、不拼接指令自带的那串英文提示词
|
|
229
231
|
|
|
230
232
|
> 之前只有 `custom` 类型的指令才合并用户输入,普通指令(手办化、coser化…)只拿它去选图、
|
|
231
233
|
> 画图时丢掉,所以会出现「图选对了但需求没体现」。现在两者都合并。
|
|
232
234
|
>
|
|
233
235
|
> 另外文本取的是**指令参数**,不是整条消息——否则提示词里会混进指令名(「手办化 xxx」→ 只取 xxx)。
|
|
234
236
|
|
|
235
|
-
### 🪄
|
|
237
|
+
### 🪄 提示词融合方式(默认:直接说人话,不多加工)
|
|
238
|
+
|
|
239
|
+
> **现在的绘图模型听得懂中文,也有基本的理解力。** 把用户那句话原样交给它就够了 ——
|
|
240
|
+
> 反过来,把它「优化」成一两百字的英文术语堆砌,各要素互相稀释,出图反而更容易走样。
|
|
241
|
+
> 所以 2.0.3 起默认是**做减法**。
|
|
236
242
|
|
|
237
243
|
| 配置项 | 说明 | 默认 |
|
|
238
244
|
|---|---|---|
|
|
239
|
-
| 附加需求如何并入提示词 | `
|
|
245
|
+
| 附加需求如何并入提示词 | `passthrough` 原话直出 / `merge` 追加 / `rewrite` 融合重写 / `off` 不处理 | **`passthrough`** |
|
|
240
246
|
| 把用户附加需求并入提示词 | 关闭则只用于选图,不改提示词 | 开 |
|
|
241
|
-
| 融合重写用的提示词模板 | 占位符 `{prompt}` 原始提示词、`{userInput}` 用户附加需求 |
|
|
247
|
+
| 融合重写用的提示词模板 | 占位符 `{prompt}` 原始提示词、`{userInput}` 用户附加需求 | 内置模板(已改成「宁短勿长」) |
|
|
248
|
+
|
|
249
|
+
- **`passthrough`(原话直出,推荐)**:用户说了话就用**他的原话**,不再拼指令自带的长提示词、
|
|
250
|
+
不扩写、不翻译。用户没说话时才退回指令自带的提示词
|
|
251
|
+
- **`merge`(追加)**:把用户的原话贴在指令自带提示词后面,不做改写
|
|
252
|
+
- **`rewrite`(融合重写)**:让模型把需求写进提示词对应位置 —— **会明显变长**,
|
|
253
|
+
老版本(≤2.0.2)就是默认这个。想要旧行为选它
|
|
254
|
+
- **`off`**:只用指令自身的提示词,忽略用户说的话(agent 模式下仍会把需求交给模型看)
|
|
242
255
|
|
|
243
|
-
|
|
244
|
-
而不是贴在最后。模板要求它**保留原始提示词的全部细节、不得删减**,语言与原文一致
|
|
245
|
-
- **merge(直接追加)**:把需求贴在原始提示词末尾,不做改写
|
|
246
|
-
- **off**:只用指令自身的提示词(agent 模式下仍会把需求交给模型看)
|
|
256
|
+
`rewrite` 失败时自动退回用户的原话,不会因为优化失败而不出图;失败会在处理消息里提示。
|
|
247
257
|
|
|
248
|
-
|
|
258
|
+
> **agent 模式下**(默认开启)这条配置项不影响你的输入是否被保留 ——
|
|
259
|
+
> 用户的话会原封不动地出现在给模型的上下文里(`用户这次说的话:…`),
|
|
260
|
+
> 画图的 prompt 由模型自己组织。2.0.3 起提示词里明确要求它
|
|
261
|
+
> **「短、说人话、保持中文、不要替用户脑补细节」**。
|
|
249
262
|
「(提示词融合失败,已改为追加模式)」,方便排查(多半是模型限流或返回被判空)。
|
|
250
263
|
用的是「AI 模型接口」那套接口/模型(想换更强的模型可以单独改)。
|
|
251
264
|
|
|
@@ -460,6 +473,31 @@ max_tokens=8000 → finish_reason: stop, content: "{...}", reasoning_tokens:
|
|
|
460
473
|
每一轮的模型输出与工具调用都会写进日志,一眼就能看出是模型没调工具、
|
|
461
474
|
还是调了但参数不对。
|
|
462
475
|
|
|
476
|
+
### 🩺 排查「画不出来 / 接口报错」
|
|
477
|
+
|
|
478
|
+
失败时插件会把**服务端返回的真实原因**原样带出来,先看这一句再决定怎么办:
|
|
479
|
+
|
|
480
|
+
```
|
|
481
|
+
[I] 请求失败(第 3/3 次):Internal Server Error|接口返回:connect ECONNREFUSED 192.168.0.85:20000
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
群里收到的失败消息里同样会带上这句。**这串东西不是乱码**,它通常直接说明问题在哪:
|
|
485
|
+
|
|
486
|
+
| 看到的说法 | 含义 | 怎么办 |
|
|
487
|
+
| --- | --- | --- |
|
|
488
|
+
| `connect ECONNREFUSED <ip>:<端口>` | 中转站的后端节点没起来 / 挂了 | **跟接口提供方无关也不在你这边**,换模型或等它恢复 |
|
|
489
|
+
| `当前模型负载较高` | 该模型现在挤,不是配置问题 | 换个负载均衡点的模型,或稍后再试 |
|
|
490
|
+
| `Model Not Exist` | 模型名写错了 | 核对模型名(大小写也算) |
|
|
491
|
+
| `Invalid API key` | 密钥不对或已过期 | 换密钥 |
|
|
492
|
+
| `insufficient_quota` | 额度没了 | 充值后会立刻恢复,插件不会傻重试 |
|
|
493
|
+
|
|
494
|
+
> 同一个中转站上**所有**绘图模型都报同一个 `ECONNREFUSED` 时,基本可以确定是对方整条链路挂了,
|
|
495
|
+
> 换个模型名也没用,只能等它恢复。判断是否全挂:换 2~3 个不同的绘图模型各试一次,
|
|
496
|
+
> 如果都指向同一个 IP 端口,就是服务端的问题。
|
|
497
|
+
|
|
498
|
+
另外,`429 限流` 和 `5xx 服务端错误` 现在是**指数退避**重试(1s → 2s → 4s…),
|
|
499
|
+
不再用固定间隔连着猛打 —— 对端真的在重启的时候,等久一点反而更容易成功。
|
|
500
|
+
|
|
463
501
|
### ❌ 不会重试的错误
|
|
464
502
|
|
|
465
503
|
`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
|
|
@@ -235,7 +256,8 @@ export function buildAgentTools(): any[] {
|
|
|
235
256
|
parameters: {
|
|
236
257
|
type: 'object',
|
|
237
258
|
properties: {
|
|
238
|
-
prompt
|
|
259
|
+
// 提示词要短:宁可朴素,也不要堆术语写成几百字的「专业」prompt
|
|
260
|
+
prompt: { type: 'string', description: '绘图提示词。说人话、写简短就行,中文即可(绘图模型听得懂中文);不要把用户的话扩写成几百字,也不要堆砌风格/材质/光线术语。画面上必须出现的文字要原样写进去' },
|
|
239
261
|
references: {
|
|
240
262
|
type: 'array',
|
|
241
263
|
items: { type: 'string' },
|
|
@@ -946,7 +968,7 @@ interface CommandConfig {
|
|
|
946
968
|
showPrompt?: boolean
|
|
947
969
|
promptMaxLength?: number
|
|
948
970
|
appendUserInput?: boolean
|
|
949
|
-
promptOptimize?: 'off' | 'merge' | 'rewrite'
|
|
971
|
+
promptOptimize?: 'off' | 'passthrough' | 'merge' | 'rewrite'
|
|
950
972
|
optimizePrompt?: string
|
|
951
973
|
resultGallery?: ResultGalleryConfig
|
|
952
974
|
backgroundDrawing?: BackgroundDrawingConfig
|
|
@@ -1113,13 +1135,15 @@ interface CandidateImage {
|
|
|
1113
1135
|
/** 把用户附加需求融合进原始提示词(而不是贴在最末尾) */
|
|
1114
1136
|
const DEFAULT_OPTIMIZE_PROMPT = `你是绘图提示词优化助手。下面是「绘图指令的原始提示词」和「用户本次的需求」。
|
|
1115
1137
|
|
|
1116
|
-
|
|
1138
|
+
请输出一份**简短**的、可直接用于绘图模型的提示词。规则:
|
|
1117
1139
|
|
|
1118
|
-
1.
|
|
1119
|
-
2.
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1140
|
+
1. 原始提示词不为空时:把用户需求自然地融入原始提示词,保留其中的构图与风格,不要删减。
|
|
1141
|
+
2. 原始提示词为空时:直接整理用户的话作为提示词,**不要替他脑补**画风、镜头、光线、氛围、配色之类的细节 ——
|
|
1142
|
+
用户没提的就别写。
|
|
1143
|
+
3. 把用户需求写到它该去的位置,不要原样贴在末尾,也不要另起一段复述一遍。
|
|
1144
|
+
4. **语言跟着用户走**:用户用中文就输出中文(绘图模型听得懂中文,理解力也够),
|
|
1145
|
+
不要为了显得专业而翻成英文、不要堆砌术语。
|
|
1146
|
+
5. **宁短勿长**:只写真正要在画面上出现的东西。提示词越长各要素互相稀释,出图反而更糊。
|
|
1123
1147
|
|
|
1124
1148
|
原始提示词:
|
|
1125
1149
|
{prompt}
|
|
@@ -1163,6 +1187,11 @@ const DEFAULT_AGENT_INSTRUCTIONS = `你是 QQ 群里的绘图助手。用户发
|
|
|
1163
1187
|
6. 流程结束后用中文回一句简短的结果说明(不要复述 prompt,不要写小作文,不要再问「还要我做什么」)。
|
|
1164
1188
|
7. 图片编号:gallery_search 结果里的 id、用户发的图(user1、user2…)、指令默认图(cmd1…)都可以直接写进 draw 的 references。
|
|
1165
1189
|
只填你确认存在的编号,**绝对不要编造 id**。
|
|
1190
|
+
8. 【prompt 要短,要说人话】**不要**把用户的话扩写成几百字的「专业」提示词:
|
|
1191
|
+
- 用户用中文提的需求,prompt 就写中文(绘图模型听得懂中文,理解力也够用);
|
|
1192
|
+
- 别为了显得专业就去堆风格 / 材质 / 光线 / 镜头术语,更别刻意翻成英文;
|
|
1193
|
+
- 只保留用户真正说过的内容 + 画面必须有的要素,**不要替他脑补细节**;
|
|
1194
|
+
- 提示词越长,各要素互相稀释,出图反而越走样。一两句话能把画面说清就够了。
|
|
1166
1195
|
|
|
1167
1196
|
如果你所在的接口不支持函数调用,就**只输出一行 JSON** 来调用工具,例如:
|
|
1168
1197
|
{"tool":"gallery_search","args":{"keyword":"白发 立绘"}}
|
|
@@ -1740,10 +1769,11 @@ export const Config: Schema = Schema.intersect([
|
|
|
1740
1769
|
Schema.object({
|
|
1741
1770
|
appendUserInput: Schema.boolean().default(true).description('把用户随指令发的附加需求并入绘图提示词(例如「手办化 xxx 在偷吃白饭被发现的表情」)'),
|
|
1742
1771
|
promptOptimize: Schema.union([
|
|
1743
|
-
Schema.const('
|
|
1744
|
-
Schema.const('merge').description('
|
|
1745
|
-
Schema.const('
|
|
1746
|
-
|
|
1772
|
+
Schema.const('passthrough').description('原话直出(推荐):用户说了就用用户的原话,不再拼接指令自带的提示词,也不做扩写'),
|
|
1773
|
+
Schema.const('merge').description('追加:把用户的原话贴在指令自带提示词后面'),
|
|
1774
|
+
Schema.const('rewrite').description('融合重写:让模型把需求写进提示词对应位置(会变长,老版本的默认行为)'),
|
|
1775
|
+
Schema.const('off').description('不处理:只用指令自带的提示词,忽略用户说的话')
|
|
1776
|
+
]).default('passthrough').description('用户附加需求如何并入提示词。**绘图模型听得懂中文**,一般不需要再优化/翻译'),
|
|
1747
1777
|
optimizePrompt: Schema.string().role('textarea', { rows: [10, 6] }).default(DEFAULT_OPTIMIZE_PROMPT).description('融合重写用的提示词模板,占位符:{prompt} 原始提示词、{userInput} 用户附加需求'),
|
|
1748
1778
|
showPrompt: Schema.boolean().default(true).description('发送优化后的提示词(QQ / QQ 频道用代码块包裹,其它平台发纯文本)'),
|
|
1749
1779
|
promptMaxLength: Schema.number().default(4000).min(0).max(20000).step(100).description('回显提示词的最大字符数,超出部分截断并标注;设为 0 表示不截断(QQ markdown 有长度上限,太长可能被拒收)'),
|
|
@@ -2303,8 +2333,16 @@ export function apply(ctx: Context, config: CommandConfig) {
|
|
|
2303
2333
|
references: capped,
|
|
2304
2334
|
...(unknown.length ? { unknown } : {}),
|
|
2305
2335
|
...(res.ok
|
|
2306
|
-
?
|
|
2307
|
-
|
|
2336
|
+
? (res.queued
|
|
2337
|
+
// 后台绘图:这里只是排上队了,图还没开始画,绝不能让模型告诉用户「画好了」
|
|
2338
|
+
? { queued: true, note: '绘图任务已排进后台队列,还没有画完。请告诉用户「已经排上队,画好后会在这里通知」,不要说已经画好或已经发出图片' }
|
|
2339
|
+
: { sent: true, note: '图已经发到群里了,回一句简短说明即可,不要再问「还要我做什么」' })
|
|
2340
|
+
: {
|
|
2341
|
+
error: res.error === 'invalidimage'
|
|
2342
|
+
? '这些参考图一张都下载不下来,检查链接还能不能访问'
|
|
2343
|
+
: `绘图接口这次没出图:${(res.reason || '未知原因').slice(0, 160)}`,
|
|
2344
|
+
note: '如实告诉用户这次没画出来、可能是什么原因,并建议稍后再试或换个模型,不要说已经画好了',
|
|
2345
|
+
}),
|
|
2308
2346
|
})
|
|
2309
2347
|
}
|
|
2310
2348
|
}
|
|
@@ -2428,7 +2466,7 @@ export function apply(ctx: Context, config: CommandConfig) {
|
|
|
2428
2466
|
imageUrls: string[],
|
|
2429
2467
|
userInputText: string,
|
|
2430
2468
|
options: { optimizeFailed?: boolean, requireInput?: boolean } = {}
|
|
2431
|
-
): Promise<{ ok: boolean, error?: string, url?: string }> {
|
|
2469
|
+
): Promise<{ ok: boolean, error?: string, url?: string, reason?: string, queued?: boolean }> {
|
|
2432
2470
|
const commandName = cmdConfig.name
|
|
2433
2471
|
const requireInput = options.requireInput !== false
|
|
2434
2472
|
const files = await downloadFiles(imageUrls)
|
|
@@ -2474,18 +2512,26 @@ export function apply(ctx: Context, config: CommandConfig) {
|
|
|
2474
2512
|
failed: session.text('image-prompt.messages.failed'),
|
|
2475
2513
|
error: session.text('image-prompt.messages.error'),
|
|
2476
2514
|
},
|
|
2515
|
+
/** 带上接口给的真实原因,用户一看就知道是模型那边的问题还是自己配置的问题 */
|
|
2516
|
+
formatFailed: (reason?: string) => {
|
|
2517
|
+
const head = String(session.text('image-prompt.messages.failed') || '').replace(/[,,。]\s*$/, '')
|
|
2518
|
+
if (!reason) return `${head}。`
|
|
2519
|
+
const clean = String(reason).replace(/\s+/g, ' ').trim()
|
|
2520
|
+
return `${head}(${clean.slice(0, 120)})`
|
|
2521
|
+
},
|
|
2477
2522
|
}
|
|
2478
2523
|
const queued = enqueueDrawJob(snapshot, files)
|
|
2479
2524
|
await sendNotice(session, `${session.text('image-prompt.messages.bgstart', [files.length])}${queued > 0 ? session.text('image-prompt.messages.bgqueue', [queued]) : ''}`)
|
|
2480
|
-
|
|
2525
|
+
// 注意:这里是「排上队了」,不是「画好了」。别让上层当成已完成通知用户
|
|
2526
|
+
return { ok: true, queued: true }
|
|
2481
2527
|
}
|
|
2482
2528
|
|
|
2483
|
-
const
|
|
2484
|
-
if (!
|
|
2529
|
+
const gen = await generateFigureImage(files, text)
|
|
2530
|
+
if (!gen.url) return { ok: false, error: 'failed', reason: gen.reason }
|
|
2485
2531
|
|
|
2486
|
-
await saveToGallery(
|
|
2487
|
-
await sendResultImage(session,
|
|
2488
|
-
return { ok: true, url:
|
|
2532
|
+
await saveToGallery(gen.url, buildGalleryDescription(text, userInputText), commandName)
|
|
2533
|
+
await sendResultImage(session, gen.url, commandName)
|
|
2534
|
+
return { ok: true, url: gen.url }
|
|
2489
2535
|
}
|
|
2490
2536
|
|
|
2491
2537
|
/** 发一条状态说明(支持的平台用 markdown,省得长文被拆) */
|
|
@@ -2504,22 +2550,30 @@ export function apply(ctx: Context, config: CommandConfig) {
|
|
|
2504
2550
|
) {
|
|
2505
2551
|
const waitTimeout = cmdConfig.waitTimeout || config.defaultWaitTimeout
|
|
2506
2552
|
const maxImages = cmdConfig.maxImages || 0
|
|
2507
|
-
|
|
2553
|
+
const basePrompt = cmdConfig.prompt || ''
|
|
2554
|
+
const input = String(userInputText || '').trim()
|
|
2555
|
+
const mode = config.promptOptimize || 'passthrough'
|
|
2556
|
+
const takeInput = input && config.appendUserInput !== false
|
|
2557
|
+
// 绘图模型听得懂中文,默认做的是「减法」:用户说了就用他的原话,
|
|
2558
|
+
// 不拼指令自带的长提示词、不扩写、不翻译。
|
|
2559
|
+
let promptText = mode === 'passthrough' ? ((takeInput ? input : '') || basePrompt) : basePrompt
|
|
2508
2560
|
let optimizeFailed = false
|
|
2509
2561
|
|
|
2510
|
-
if (
|
|
2511
|
-
promptText = mergePrompt(
|
|
2512
|
-
logInfo(
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2562
|
+
if (takeInput && mode === 'merge') {
|
|
2563
|
+
promptText = mergePrompt(basePrompt, input, true)
|
|
2564
|
+
logInfo(`已把用户原话追加到指令提示词后: ${input}`)
|
|
2565
|
+
} else if (takeInput && mode === 'rewrite') {
|
|
2566
|
+
const optimized = await optimizePromptText(basePrompt, input)
|
|
2567
|
+
if (optimized) {
|
|
2568
|
+
promptText = optimized
|
|
2569
|
+
logInfo('提示词已融合重写')
|
|
2570
|
+
} else {
|
|
2571
|
+
ctx.logger.warn('提示词融合重写失败,改用用户的原话')
|
|
2572
|
+
promptText = mergePrompt(basePrompt, input, true)
|
|
2573
|
+
optimizeFailed = true
|
|
2522
2574
|
}
|
|
2575
|
+
} else if (takeInput && mode !== 'off') {
|
|
2576
|
+
logInfo(`已并入用户原话(${mode} 模式): ${input}`)
|
|
2523
2577
|
}
|
|
2524
2578
|
|
|
2525
2579
|
// 自定义指令:本身没提示词、用户也没给文本 -> 要一句
|
|
@@ -2533,9 +2587,7 @@ export function apply(ctx: Context, config: CommandConfig) {
|
|
|
2533
2587
|
}
|
|
2534
2588
|
if (!userPrompt) return session.text('image-prompt.messages.needprompt')
|
|
2535
2589
|
const text = extractTextFromMessage(userPrompt) || String(userPrompt).trim()
|
|
2536
|
-
promptText =
|
|
2537
|
-
? ((await optimizePromptText('', text)) || text)
|
|
2538
|
-
: mergePrompt('', text, true)
|
|
2590
|
+
promptText = (mode === 'rewrite' && (await optimizePromptText('', text))) || text
|
|
2539
2591
|
}
|
|
2540
2592
|
|
|
2541
2593
|
const images: string[] = [...(defaultImageUrls || []), ...extractImagesFromSession(session)]
|
|
@@ -2565,7 +2617,9 @@ export function apply(ctx: Context, config: CommandConfig) {
|
|
|
2565
2617
|
if (!res.ok) {
|
|
2566
2618
|
return res.error === 'invalidimage'
|
|
2567
2619
|
? session.text('image-prompt.messages.invalidimage')
|
|
2568
|
-
:
|
|
2620
|
+
: res.reason
|
|
2621
|
+
? `${String(session.text('image-prompt.messages.failed') || '').replace(/[,,。]\s*$/, '')}(${res.reason.replace(/\s+/g, ' ').trim().slice(0, 120)})`
|
|
2622
|
+
: session.text('image-prompt.messages.failed')
|
|
2569
2623
|
}
|
|
2570
2624
|
}
|
|
2571
2625
|
// ============ 后台绘图 ============
|
|
@@ -2598,13 +2652,13 @@ export function apply(ctx: Context, config: CommandConfig) {
|
|
|
2598
2652
|
|
|
2599
2653
|
try {
|
|
2600
2654
|
if (!isActive || !ctx.scope.isActive) return
|
|
2601
|
-
const
|
|
2602
|
-
if (!
|
|
2603
|
-
await send([snapshot.texts.failed])
|
|
2655
|
+
const gen = await generateFigureImage(files, snapshot.promptText)
|
|
2656
|
+
if (!gen.url) {
|
|
2657
|
+
await send([snapshot.formatFailed ? snapshot.formatFailed(gen.reason) : snapshot.texts.failed])
|
|
2604
2658
|
return
|
|
2605
2659
|
}
|
|
2606
|
-
await saveToGallery(
|
|
2607
|
-
await send([`${snapshot.texts.done}\n`, await buildResultImagePart(
|
|
2660
|
+
await saveToGallery(gen.url, buildGalleryDescription(snapshot.promptText, ''), snapshot.commandName)
|
|
2661
|
+
await send([`${snapshot.texts.done}\n`, await buildResultImagePart(gen.url)])
|
|
2608
2662
|
} catch (error) {
|
|
2609
2663
|
ctx.logger.error(`[${snapshot.commandName}] 后台绘图失败:`, error)
|
|
2610
2664
|
try {
|
|
@@ -3273,7 +3327,8 @@ export function apply(ctx: Context, config: CommandConfig) {
|
|
|
3273
3327
|
return images
|
|
3274
3328
|
}
|
|
3275
3329
|
|
|
3276
|
-
|
|
3330
|
+
/** @returns 图片地址;失败时带回接口给的真实原因,方便直接告诉用户「是模型那边的毛病」 */
|
|
3331
|
+
async function generateFigureImage(files: any[], prompt: string): Promise<{ url: string | null, reason?: string }> {
|
|
3277
3332
|
try {
|
|
3278
3333
|
const dataUrls: string[] = []
|
|
3279
3334
|
|
|
@@ -3352,22 +3407,24 @@ export function apply(ctx: Context, config: CommandConfig) {
|
|
|
3352
3407
|
return await sendChatRequest(requestBody)
|
|
3353
3408
|
} catch (error) {
|
|
3354
3409
|
ctx.logger.error(`生成图片时发生错误: ${error}`)
|
|
3355
|
-
return null
|
|
3410
|
+
return { url: null, reason: String(error?.message || error) }
|
|
3356
3411
|
}
|
|
3357
3412
|
}
|
|
3358
3413
|
|
|
3359
|
-
async function sendChatRequest(requestBody: any): Promise<string | null> {
|
|
3414
|
+
async function sendChatRequest(requestBody: any): Promise<{ url: string | null, reason?: string }> {
|
|
3360
3415
|
let retryCount = 0
|
|
3416
|
+
const maxAttempts = (config.maxRetries ?? 0) + 1
|
|
3417
|
+
let lastReason = ''
|
|
3361
3418
|
|
|
3362
|
-
while (retryCount
|
|
3419
|
+
while (retryCount < maxAttempts) {
|
|
3363
3420
|
// 在每次重试前检查上下文状态
|
|
3364
3421
|
if (!isActive || !ctx.scope.isActive) {
|
|
3365
3422
|
ctx.logger.info('插件已卸载,停止重试')
|
|
3366
|
-
return null
|
|
3423
|
+
return { url: null, reason: lastReason }
|
|
3367
3424
|
}
|
|
3368
3425
|
|
|
3369
3426
|
try {
|
|
3370
|
-
logInfo(`发送请求到 ${config.baseUrl},第 ${retryCount + 1} 次尝试`)
|
|
3427
|
+
logInfo(`发送请求到 ${config.baseUrl},第 ${retryCount + 1}/${maxAttempts} 次尝试`)
|
|
3371
3428
|
|
|
3372
3429
|
const headers: Record<string, string> = {
|
|
3373
3430
|
'Content-Type': 'application/json'
|
|
@@ -3389,40 +3446,41 @@ export function apply(ctx: Context, config: CommandConfig) {
|
|
|
3389
3446
|
if (markdownMatch && markdownMatch[1]) {
|
|
3390
3447
|
const imageUrl = markdownMatch[1]
|
|
3391
3448
|
logInfo(`成功获取图片URL: ${imageUrl}`)
|
|
3392
|
-
return imageUrl
|
|
3449
|
+
return { url: imageUrl }
|
|
3393
3450
|
}
|
|
3394
3451
|
}
|
|
3395
3452
|
}
|
|
3396
3453
|
|
|
3397
|
-
|
|
3398
|
-
throw new Error(errorMsg)
|
|
3454
|
+
throw new Error('响应中未找到图片URL')
|
|
3399
3455
|
} catch (error) {
|
|
3400
3456
|
retryCount++
|
|
3401
|
-
const
|
|
3402
|
-
|
|
3457
|
+
const reason = httpErrorText(error)
|
|
3458
|
+
lastReason = reason
|
|
3459
|
+
const statusCode = error.response?.status || error.status || error.code || 0
|
|
3403
3460
|
|
|
3404
|
-
logInfo(
|
|
3461
|
+
logInfo(`请求失败(第 ${retryCount}/${maxAttempts} 次):${reason}`)
|
|
3405
3462
|
|
|
3406
3463
|
// 检查是否为配额不足错误
|
|
3407
|
-
if (
|
|
3464
|
+
if (reason.includes('insufficient_quota') || statusCode === 429) {
|
|
3408
3465
|
ctx.logger.error('API 配额不足,停止重试')
|
|
3409
|
-
return null
|
|
3466
|
+
return { url: null, reason }
|
|
3410
3467
|
}
|
|
3411
3468
|
|
|
3412
|
-
if (retryCount
|
|
3413
|
-
|
|
3414
|
-
|
|
3469
|
+
if (retryCount < maxAttempts) {
|
|
3470
|
+
// 服务端错误/网络错误走指数退避,别用固定间隔猛打
|
|
3471
|
+
const delay = computeRetryDelay(statusCode, undefined, config.retryInterval, retryCount - 1)
|
|
3472
|
+
logInfo(`${delay}ms 后重试`)
|
|
3473
|
+
await sleep(delay)
|
|
3415
3474
|
if (!isActive || !ctx.scope.isActive) {
|
|
3416
3475
|
ctx.logger.info('插件已卸载,停止重试')
|
|
3417
|
-
return null
|
|
3476
|
+
return { url: null, reason }
|
|
3418
3477
|
}
|
|
3419
3478
|
} else {
|
|
3420
|
-
ctx.logger.error(
|
|
3421
|
-
return null
|
|
3479
|
+
ctx.logger.error(`已重试 ${maxAttempts} 次仍失败,最后错误:${reason}`)
|
|
3422
3480
|
}
|
|
3423
3481
|
}
|
|
3424
3482
|
}
|
|
3425
|
-
return null
|
|
3483
|
+
return { url: null, reason: lastReason }
|
|
3426
3484
|
}
|
|
3427
3485
|
|
|
3428
3486
|
function logInfo(...args: any[]) {
|