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 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
- if (status === 429 || status === 503) {
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: { type: "string", description: "\u5B8C\u6574\u7684\u7ED8\u56FE\u63D0\u793A\u8BCD\uFF08\u82F1\u6587\u6548\u679C\u66F4\u7A33\uFF1B\u753B\u9762\u4E0A\u5FC5\u987B\u51FA\u73B0\u7684\u6587\u5B57\u8981\u539F\u6837\u5199\u8FDB\u53BB\uFF09" },
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\u5B8C\u6574\u3001\u53EF\u76F4\u63A5\u7528\u4E8E\u7ED8\u56FE\u6A21\u578B\u7684\u63D0\u793A\u8BCD\u3002\u89C4\u5219\uFF1A
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\u5408\u8FDB\u539F\u59CB\u63D0\u793A\u8BCD\uFF0C\u5E76**\u4FDD\u7559\u539F\u59CB\u63D0\u793A\u8BCD\u4E2D\u7684\u5168\u90E8\u573A\u666F\u3001\u6784\u56FE\u3001\u98CE\u683C\u3001\u6750\u8D28\u3001\u5149\u7EBF\u7B49\u7EC6\u8282**\uFF0C\u4E0D\u5F97\u5220\u51CF\u6216\u7B80\u5316\u3002
751
- 2. \u539F\u59CB\u63D0\u793A\u8BCD\u4E3A\u7A7A\u65F6\uFF08\u4F8B\u5982\u81EA\u5B9A\u4E49\u6307\u4EE4\uFF09\uFF1A\u6839\u636E\u7528\u6237\u9700\u6C42**\u6269\u5199**\u6210\u5B8C\u6574\u7684\u7ED8\u56FE\u63D0\u793A\u8BCD\uFF0C\u8865\u8DB3\u753B\u98CE\u3001\u6784\u56FE\u3001\u955C\u5934\u3001\u5149\u7EBF\u3001\u6C1B\u56F4\u3001\u914D\u8272\u4E0E\u7EC6\u8282\uFF0C\u4F46\u4E0D\u8981\u6539\u53D8\u7528\u6237\u7684\u539F\u610F\u3002
752
- 3. \u628A\u7528\u6237\u9700\u6C42\u5199\u5230\u5B83\u8BE5\u53BB\u7684\u4F4D\u7F6E\uFF08\u8868\u60C5/\u52A8\u4F5C/\u795E\u6001/\u670D\u88C5/\u573A\u666F/\u4E92\u52A8\u7B49\uFF09\uFF0C\u4E0D\u8981\u539F\u6837\u8D34\u5728\u672B\u5C3E\u3002
753
- 4. \u8BED\u8A00\uFF1A\u539F\u59CB\u63D0\u793A\u8BCD\u662F\u82F1\u6587\u5C31\u8F93\u51FA\u82F1\u6587\uFF1B\u539F\u59CB\u63D0\u793A\u8BCD\u4E3A\u7A7A\u65F6\u4E5F\u7528\u82F1\u6587\uFF08\u82F1\u6587\u63D0\u793A\u8BCD\u51FA\u56FE\u6548\u679C\u901A\u5E38\u66F4\u7A33\uFF09\u3002
754
- 5. \u53EA\u8F93\u51FA\u4F18\u5316\u540E\u7684\u63D0\u793A\u8BCD\u6B63\u6587\uFF0C\u4E0D\u8981\u89E3\u91CA\u3001\u4E0D\u8981\u6807\u9898\u3001\u4E0D\u8981 Markdown \u4EE3\u7801\u5757\u3001\u4E0D\u8981\u5F15\u53F7\u3002
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("rewrite").description("\u878D\u5408\u91CD\u5199\uFF08\u63A8\u8350\uFF09\uFF1A\u8BA9\u6A21\u578B\u628A\u9700\u6C42\u5199\u8FDB\u63D0\u793A\u8BCD\u5BF9\u5E94\u4F4D\u7F6E"),
1353
- import_koishi.Schema.const("merge").description("\u76F4\u63A5\u8FFD\u52A0\uFF1A\u628A\u9700\u6C42\u8D34\u5728\u539F\u59CB\u63D0\u793A\u8BCD\u672B\u5C3E"),
1354
- import_koishi.Schema.const("off").description("\u4E0D\u5904\u7406\uFF1A\u53EA\u7528\u6307\u4EE4\u81EA\u8EAB\u7684\u63D0\u793A\u8BCD")
1355
- ]).default("rewrite").description("\u7528\u6237\u9644\u52A0\u9700\u6C42\u5982\u4F55\u5E76\u5165\u63D0\u793A\u8BCD"),
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\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" } : { 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\u6CA1\u6709\u8FD4\u56DE\u56FE\u7247" }
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 result = await generateFigureImage(files, text);
1905
- if (!result) return { ok: false, error: "failed" };
1906
- await saveToGallery(result, buildGalleryDescription(text, userInputText), commandName);
1907
- await sendResultImage(session, result, commandName);
1908
- return { ok: true, url: result };
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
- let promptText = cmdConfig.prompt;
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 (userInputText && config.appendUserInput !== false) {
1921
- promptText = mergePrompt(promptText, userInputText, true);
1922
- logInfo2(`\u5DF2\u5E76\u5165\u7528\u6237\u9644\u52A0\u9700\u6C42: ${userInputText}`);
1923
- if (config.promptOptimize === "rewrite") {
1924
- const optimized = await optimizePromptText(cmdConfig.prompt, userInputText);
1925
- if (optimized) {
1926
- promptText = optimized;
1927
- logInfo2("\u63D0\u793A\u8BCD\u5DF2\u878D\u5408\u91CD\u5199");
1928
- } else {
1929
- ctx.logger.warn("\u63D0\u793A\u8BCD\u878D\u5408\u91CD\u5199\u5931\u8D25\uFF0C\u4FDD\u7559\u8FFD\u52A0\u540E\u7684\u63D0\u793A\u8BCD");
1930
- optimizeFailed = true;
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 = config.promptOptimize === "rewrite" ? await optimizePromptText("", text) || text : mergePrompt("", text, true);
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 result = await generateFigureImage(files, snapshot.promptText);
1998
- if (!result) {
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(result, buildGalleryDescription(snapshot.promptText, ""), snapshot.commandName);
2039
+ await saveToGallery(gen.url, buildGalleryDescription(snapshot.promptText, ""), snapshot.commandName);
2003
2040
  await send([`${snapshot.texts.done}
2004
- `, await buildResultImagePart(result)]);
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
- while (retryCount <= config.maxRetries) {
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
- const errorMsg = "\u54CD\u5E94\u4E2D\u672A\u627E\u5230\u56FE\u7247URL";
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 errorMessage = error.message || error.toString();
2595
- const statusCode = error.response?.status || 0;
2596
- logInfo2(`\u8BF7\u6C42\u5931\u8D25 (${retryCount}/${config.maxRetries}): ${errorMessage}`);
2597
- if (errorMessage.includes("insufficient_quota") || statusCode === 429) {
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 <= config.maxRetries) {
2602
- logInfo2(`\u7B49\u5F85 ${config.retryInterval}ms \u540E\u91CD\u8BD5`);
2603
- await (0, import_koishi.sleep)(config.retryInterval);
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(`\u8FBE\u5230\u6700\u5927\u91CD\u8BD5\u6B21\u6570 (${config.maxRetries})\uFF0C\u6700\u540E\u9519\u8BEF: ${errorMessage}`);
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.1",
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
- 处理时会把**最终优化后的提示词**一起发出来,方便你确认 AI 到底用了什么(含识图模式追加的「参考图关键特征」)。
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 选图(按「deepseekQ版」等关键词去参考图组里挑图)
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
- | 附加需求如何并入提示词 | `rewrite` 融合重写 / `merge` 直接追加 / `off` 不处理 | `rewrite` |
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
- - **rewrite(融合重写,推荐)**:让模型把需求写进提示词**该去的位置**(表情/动作/神态/服装/场景),
244
- 而不是贴在最后。模板要求它**保留原始提示词的全部细节、不得删减**,语言与原文一致
245
- - **merge(直接追加)**:把需求贴在原始提示词末尾,不做改写
246
- - **off**:只用指令自身的提示词(agent 模式下仍会把需求交给模型看)
256
+ `rewrite` 失败时自动退回用户的原话,不会因为优化失败而不出图;失败会在处理消息里提示。
247
257
 
248
- rewrite 失败时自动退回 merge 的结果,不会因为优化失败而不出图;失败会在处理消息里提示
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
- if (status === 429 || status === 503) {
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: { type: 'string', description: '完整的绘图提示词(英文效果更稳;画面上必须出现的文字要原样写进去)' },
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
- 3. 把用户需求写到它该去的位置(表情/动作/神态/服装/场景/互动等),不要原样贴在末尾。
1121
- 4. 语言:原始提示词是英文就输出英文;原始提示词为空时也用英文(英文提示词出图效果通常更稳)。
1122
- 5. 只输出优化后的提示词正文,不要解释、不要标题、不要 Markdown 代码块、不要引号。
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('rewrite').description('融合重写(推荐):让模型把需求写进提示词对应位置'),
1744
- Schema.const('merge').description('直接追加:把需求贴在原始提示词末尾'),
1745
- Schema.const('off').description('不处理:只用指令自身的提示词')
1746
- ]).default('rewrite').description('用户附加需求如何并入提示词'),
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
- ? { note: '图已经发到群里了,回一句简短说明即可,不要再问「还要我做什么」' }
2307
- : { error: res.error === 'invalidimage' ? '这些参考图一张都下载不下来,检查链接还能不能访问' : '绘图接口没有返回图片' }),
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
- return { ok: true }
2525
+ // 注意:这里是「排上队了」,不是「画好了」。别让上层当成已完成通知用户
2526
+ return { ok: true, queued: true }
2481
2527
  }
2482
2528
 
2483
- const result = await generateFigureImage(files, text)
2484
- if (!result) return { ok: false, error: 'failed' }
2529
+ const gen = await generateFigureImage(files, text)
2530
+ if (!gen.url) return { ok: false, error: 'failed', reason: gen.reason }
2485
2531
 
2486
- await saveToGallery(result, buildGalleryDescription(text, userInputText), commandName)
2487
- await sendResultImage(session, result, commandName)
2488
- return { ok: true, url: result }
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
- let promptText = cmdConfig.prompt
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 (userInputText && config.appendUserInput !== false) {
2511
- promptText = mergePrompt(promptText, userInputText, true)
2512
- logInfo(`已并入用户附加需求: ${userInputText}`)
2513
- if (config.promptOptimize === 'rewrite') {
2514
- const optimized = await optimizePromptText(cmdConfig.prompt, userInputText)
2515
- if (optimized) {
2516
- promptText = optimized
2517
- logInfo('提示词已融合重写')
2518
- } else {
2519
- ctx.logger.warn('提示词融合重写失败,保留追加后的提示词')
2520
- optimizeFailed = true
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 = config.promptOptimize === 'rewrite'
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
- : session.text('image-prompt.messages.failed')
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 result = await generateFigureImage(files, snapshot.promptText)
2602
- if (!result) {
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(result, buildGalleryDescription(snapshot.promptText, ''), snapshot.commandName)
2607
- await send([`${snapshot.texts.done}\n`, await buildResultImagePart(result)])
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
- async function generateFigureImage(files: any[], prompt: string): Promise<string | null> {
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 <= config.maxRetries) {
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
- const errorMsg = '响应中未找到图片URL'
3398
- throw new Error(errorMsg)
3454
+ throw new Error('响应中未找到图片URL')
3399
3455
  } catch (error) {
3400
3456
  retryCount++
3401
- const errorMessage = error.message || error.toString()
3402
- const statusCode = error.response?.status || 0
3457
+ const reason = httpErrorText(error)
3458
+ lastReason = reason
3459
+ const statusCode = error.response?.status || error.status || error.code || 0
3403
3460
 
3404
- logInfo(`请求失败 (${retryCount}/${config.maxRetries}): ${errorMessage}`)
3461
+ logInfo(`请求失败(第 ${retryCount}/${maxAttempts} 次):${reason}`)
3405
3462
 
3406
3463
  // 检查是否为配额不足错误
3407
- if (errorMessage.includes('insufficient_quota') || statusCode === 429) {
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 <= config.maxRetries) {
3413
- logInfo(`等待 ${config.retryInterval}ms 后重试`)
3414
- await sleep(config.retryInterval)
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(`达到最大重试次数 (${config.maxRetries}),最后错误: ${errorMessage}`)
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[]) {