koishi-plugin-image-prompt 1.2.0 → 1.3.0

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
@@ -9,7 +9,7 @@ export declare const inject: {
9
9
  required: string[];
10
10
  optional: string[];
11
11
  };
12
- export declare const usage = "\n---\n\n\u6B64\u63D2\u4EF6\u76F4\u63A5\u8C03\u7528 OpenAI \u517C\u5BB9\u7684 Chat Completions \u63A5\u53E3\u751F\u6210\u56FE\u7247\n\n\u8BF7\u5728\u63D2\u4EF6\u8BBE\u7F6E\u4E2D\u586B\u5199\uFF1A\n\n- API \u670D\u52A1\u5668\u5730\u5740\uFF08baseUrl\uFF09\n- \u4F7F\u7528\u7684\u6A21\u578B\uFF08model\uFF09\n- API \u5BC6\u94A5\uFF08apiKey\uFF09\n\n\u3010AI \u9009\u62E9\u53C2\u8003\u56FE\u7247\u3011\n\n1. \u5728\u300C\u53C2\u8003\u56FE\u7247\u7EC4\u300D\u4E2D\u6CE8\u518C\u5206\u7EC4\uFF1A\u6BCF\u7EC4\u586B\u5199\u82E5\u5E72\u5F20\u300C\u56FE\u7247\u94FE\u63A5 + \u63CF\u8FF0\u300D\uFF08\u63CF\u8FF0\u7528\u4E8E\u8BA9 AI \u5224\u65AD\u8BE5\u56FE\u7684\u7528\u9014\uFF09\u3002\n **\u63CF\u8FF0\u4E00\u5B9A\u8981\u5199\u5177\u4F53**\uFF08\u53D1\u8272\u53D1\u578B\u3001\u670D\u88C5\u3001\u52A8\u4F5C\u3001\u753B\u98CE\uFF09\u2014\u2014 \u6253\u5206\u5236\u4E0B\u6CA1\u63CF\u8FF0\u7684\u56FE\u6C38\u8FDC\u9009\u4E0D\u4E0A\u3002\n2. \u5728\u6307\u4EE4\u914D\u7F6E\u7684\u300C\u5F15\u7528\u7684\u53C2\u8003\u56FE\u7247\u7EC4\u540D\u79F0\u300D\u4E2D\u586B\u5165\u7EC4\u540D\uFF08\u53EF\u586B\u591A\u4E2A\uFF09\uFF0C\u8BE5\u6307\u4EE4\u6267\u884C\u65F6\u4F1A\u628A\u7EC4\u5185\u56FE\u7247\u5168\u90E8\u4EA4\u7ED9 AI \u6311\u9009\u3002\n3. \u300CAI \u9009\u56FE\u8BBE\u7F6E\u300D\u53EF\u4FEE\u6539\u5BF9\u8BDD\u6A21\u578B\uFF08\u9ED8\u8BA4 Qwen/Qwen2.5-7B-Instruct\uFF09\u3001\u9009\u62E9\u6A21\u5F0F\u3001\u63D0\u793A\u8BCD\u6A21\u677F\u3001\u8D85\u65F6\u4E0E\u91CD\u8BD5\u7B49\uFF1B\n \u63A5\u53E3\u5730\u5740/\u5BC6\u94A5\u7559\u7A7A\u65F6\u590D\u7528\u7ED8\u56FE\u63A5\u53E3\u7684\u914D\u7F6E\u3002\n4. \u300C\u9009\u56FE\u6A21\u5F0F\u300D\u9ED8\u8BA4\u662F**\u6253\u5206\u5236**\uFF1A\u5148\u53EA\u505A\u9700\u6C42\u5206\u6790\uFF08**\u4E0D\u7ED9\u6A21\u578B\u5019\u9009\u56FE**\uFF0C\u5B83\u6CA1\u6709\u300C\u53EF\u6311\u7684\u5BF9\u8C61\u300D\uFF09\uFF0C\n \u518D\u7528\u5173\u952E\u8BCD\u5728\u672C\u5730\u53EC\u56DE\uFF0C\u7136\u540E\u8981\u6C42\u6A21\u578B\u7ED9\u6BCF\u5F20\u5019\u9009\u56FE\u6253 0-100 \u5206\uFF0C\u4F4E\u4E8E\u300C\u6700\u4F4E\u5206\u6570\u7EBF\u300D\u7684\u4E00\u5F8B\u4E0D\u8981\u3002\n \u4E00\u5F20\u90FD\u4E0D\u8FBE\u6807\u5C31\u5982\u5B9E\u544A\u77E5\u7528\u6237\u300C\u56FE\u5E93\u91CC\u6CA1\u6709\u5408\u9002\u7684\u53C2\u8003\u56FE\u300D\uFF0C**\u4E0D\u4F1A\u786C\u51D1**\uFF1B\n \u82E5\u5F00\u542F\u300C\u7F3A\u5C11\u5408\u9002\u53C2\u8003\u56FE\u65F6\u8BE2\u95EE\u7528\u6237\u8865\u5145\u53D1\u9001\u300D\uFF0C\u4F1A\u8BF7\u7528\u6237\u8865\u53D1\u4E00\u5F20\u53C2\u8003\u56FE\uFF08\u7528\u6237\u53D1\u7684\u56FE\u4F1A\u88AB\u76F4\u63A5\u4F7F\u7528\uFF09\u3002\n \u60F3\u56DE\u5230\u8001\u884C\u4E3A\u5C31\u628A\u300C\u9009\u56FE\u6A21\u5F0F\u300D\u6539\u6210\u300C\u65E7\u7248\u300D\u3002\n5. \u5F53\u7528\u6237\u660E\u786E\u8BF4\u300C\u4E0D\u7528\u53C2\u8003 / \u968F\u610F\u753B / \u81EA\u7531\u53D1\u6325\u300D\u65F6\uFF0C\u76F4\u63A5\u8DF3\u8FC7\u9009\u56FE\uFF0C\u4E00\u6B21\u9009\u56FE\u8BF7\u6C42\u90FD\u4E0D\u53D1\u3002\n6. \u300C\u542F\u7528 AI \u667A\u80FD\u9009\u62E9\u53C2\u8003\u56FE\u7247\u300D\u9ED8\u8BA4\u5173\u95ED\uFF0C\u9700\u624B\u52A8\u5F00\u542F\uFF1B\u300CAI \u9009\u56FE\u5931\u8D25\u65F6\u56DE\u9000\u4E3A\u4F7F\u7528\u5019\u9009\u6C60\u5185\u5168\u90E8\u56FE\u7247\u300D\u9ED8\u8BA4\u5173\u95ED\uFF0C\n \u5931\u8D25\u65F6\u672C\u6B21\u4E0D\u4F7F\u7528\u53C2\u8003\u56FE\u7247\uFF08\u5F00\u542F\u5219\u6539\u7528\u5019\u9009\u6C60\u5185\u5168\u90E8\u56FE\u7247\uFF09\u3002\u6CE8\u610F\u300C\u56DE\u9000\u300D\u53EA\u5BF9**\u8BF7\u6C42\u5931\u8D25**\u751F\u6548\uFF0C\n \u5BF9\u300C\u6253\u5206\u4E0D\u8FBE\u6807\u300D\u4E0D\u751F\u6548 \u2014\u2014 \u540E\u8005\u5C31\u8BE5\u4E0D\u5E26\u53C2\u8003\u56FE\u3002\n7. \u300C\u65E7\u7248\u300D\u6A21\u5F0F\u4E0B\u53C2\u8003\u56FE\u8F83\u591A\u65F6\uFF08\u8D85\u8FC7 12 \u5F20\uFF09\u81EA\u52A8\u8D70\u4E24\u7EA7\u68C0\u7D22\uFF1A\u5148\u8BA9\u6A21\u578B\u4EA7\u51FA\u68C0\u7D22\u5173\u952E\u8BCD\u3001\u672C\u5730\u5339\u914D\u53EC\u56DE\uFF0C\n \u518D\u5BF9\u53EC\u56DE\u7ED3\u679C\u7CBE\u6392\uFF1B\u8BC6\u56FE\u6A21\u5F0F\u4E0B\u4E5F\u53EA\u53D1\u9001\u53EC\u56DE\u7684\u8FD9\u51E0\u5F20\u56FE\u7247\uFF0C\u907F\u514D\u6BCF\u6B21\u90FD\u628A\u6574\u4E2A\u56FE\u5E93\u53D1\u7ED9\u6A21\u578B\u3002\n8. \u63CF\u8FF0\u53EF\u4EE5\u7528\u300C\u751F\u6210\u63CF\u8FF0\u300D\u6307\u4EE4\u8BA9\u6A21\u578B\u770B\u56FE\u81EA\u52A8\u751F\u6210\u5E76\u5199\u56DE\u914D\u7F6E\uFF08\u9700\u8981\u652F\u6301\u56FE\u7247\u8F93\u5165\u7684\u6A21\u578B\uFF09\u3002\n \u82E5\u9009\u56FE\u6A21\u578B\u652F\u6301\u8BC6\u522B\u56FE\u7247\uFF08\u591A\u6A21\u6001\uFF09\uFF0C\u53EF\u52FE\u9009\u300C\u9009\u56FE\u6A21\u578B\u652F\u6301\u8BC6\u522B\u56FE\u7247\u300D\uFF0C\u63D2\u4EF6\u4F1A\u628A\u5019\u9009\u56FE\u7247\u672C\u8EAB\u53D1\u7ED9\u6A21\u578B\uFF0C\n \u6A21\u578B\u5BF9\u7740\u771F\u5B9E\u56FE\u7247\u6311\u9009\uFF1B\u5B83\u7ED9\u51FA\u7684\u5173\u952E\u89C6\u89C9\u7279\u5F81\u8FD8\u4F1A\u5E76\u5165\u7ED8\u56FE\u63D0\u793A\u8BCD\uFF0C\u8BA9\u51FA\u56FE\u66F4\u8FD8\u539F\u53C2\u8003\u56FE\u3002\n9. \u751F\u6210\u7ED3\u679C\u53EF\u81EA\u52A8\u5165\u5E93\uFF08\u9ED8\u8BA4\u5173\u95ED\uFF09\uFF0C\u4E0B\u6B21\u80FD\u88AB\u81EA\u5DF1\u68C0\u7D22\u5230\u5E76\u590D\u7528\uFF0C\u5F62\u6210\u95ED\u73AF\uFF1B\n \u5F00\u542F\u300C\u540E\u53F0\u7ED8\u56FE\u300D\u540E\u51FA\u56FE\u4E0D\u518D\u963B\u585E\uFF0C\u5148\u56DE\u300C\u6B63\u5728\u753B\u300D\uFF0C\u753B\u597D\u4E3B\u52A8\u63A8\u9001\u3002\n\n\u63D0\u793A\u8BCD\u6A21\u677F\u53EF\u7528\u5360\u4F4D\u7B26\uFF1A{candidates} \u5019\u9009\u56FE\u7247\u5217\u8868\u3001{userInput} \u7528\u6237\u9644\u52A0\u9700\u6C42\u3001{max} \u6700\u591A\u9009\u62E9\u6570\u91CF\u3001{command} \u6307\u4EE4\u540D\u3001{prompt} \u6307\u4EE4\u63D0\u793A\u8BCD\n\n---\n\u6B64\u9879\u76EE\u6240\u9700\u7684koishi\u670D\u52A1\uFF1A\u5FC5\u9700 'http', 'logger', 'i18n'\uFF1B\u53EF\u9009 'puppeteer'\uFF08\u4EC5\u300C\u6587\u5B57\u6E32\u67D3\u53C2\u8003\u56FE\u300D\u9700\u8981\uFF09\n\n---\n";
12
+ export declare const usage = "\n---\n\n\u6B64\u63D2\u4EF6\u76F4\u63A5\u8C03\u7528 OpenAI \u517C\u5BB9\u7684 Chat Completions \u63A5\u53E3\u751F\u6210\u56FE\u7247\n\n\u8BF7\u5728\u63D2\u4EF6\u8BBE\u7F6E\u4E2D\u586B\u5199\uFF1A\n\n- API \u670D\u52A1\u5668\u5730\u5740\uFF08baseUrl\uFF09\n- \u4F7F\u7528\u7684\u6A21\u578B\uFF08model\uFF09\n- API \u5BC6\u94A5\uFF08apiKey\uFF09\n\n\u3010AI \u9009\u62E9\u53C2\u8003\u56FE\u7247\u3011\n\n1. \u5728\u300C\u53C2\u8003\u56FE\u7247\u7EC4\u300D\u4E2D\u6CE8\u518C\u5206\u7EC4\uFF1A\u6BCF\u7EC4\u586B\u5199\u82E5\u5E72\u5F20\u300C\u56FE\u7247\u94FE\u63A5 + \u63CF\u8FF0\u300D\uFF08\u63CF\u8FF0\u7528\u4E8E\u8BA9 AI \u5224\u65AD\u8BE5\u56FE\u7684\u7528\u9014\uFF09\u3002\n **\u63CF\u8FF0\u4E00\u5B9A\u8981\u5199\u5177\u4F53**\uFF08\u53D1\u8272\u53D1\u578B\u3001\u670D\u88C5\u3001\u52A8\u4F5C\u3001\u753B\u98CE\uFF09\u2014\u2014 \u6253\u5206\u5236\u4E0B\u6CA1\u63CF\u8FF0\u7684\u56FE\u6C38\u8FDC\u9009\u4E0D\u4E0A\u3002\n2. \u5728\u6307\u4EE4\u914D\u7F6E\u7684\u300C\u5F15\u7528\u7684\u53C2\u8003\u56FE\u7247\u7EC4\u540D\u79F0\u300D\u4E2D\u586B\u5165\u7EC4\u540D\uFF08\u53EF\u586B\u591A\u4E2A\uFF09\uFF0C\u8BE5\u6307\u4EE4\u6267\u884C\u65F6\u4F1A\u628A\u7EC4\u5185\u56FE\u7247\u5168\u90E8\u4EA4\u7ED9 AI \u6311\u9009\u3002\n3. \u300CAI \u9009\u56FE\u8BBE\u7F6E\u300D\u53EF\u4FEE\u6539\u5BF9\u8BDD\u6A21\u578B\uFF08\u9ED8\u8BA4 Qwen/Qwen2.5-7B-Instruct\uFF09\u3001\u9009\u62E9\u6A21\u5F0F\u3001\u63D0\u793A\u8BCD\u6A21\u677F\u3001\u8D85\u65F6\u4E0E\u91CD\u8BD5\u7B49\uFF1B\n \u63A5\u53E3\u5730\u5740/\u5BC6\u94A5\u7559\u7A7A\u65F6\u590D\u7528\u7ED8\u56FE\u63A5\u53E3\u7684\u914D\u7F6E\u3002\n4. \u300C\u9009\u56FE\u6A21\u5F0F\u300D\u9ED8\u8BA4\u662F**\u6253\u5206\u5236**\uFF1A\u5148\u53EA\u505A\u9700\u6C42\u5206\u6790\uFF08**\u4E0D\u7ED9\u6A21\u578B\u5019\u9009\u56FE**\uFF0C\u5B83\u6CA1\u6709\u300C\u53EF\u6311\u7684\u5BF9\u8C61\u300D\uFF09\uFF0C\n \u518D\u7528\u5173\u952E\u8BCD\u5728\u672C\u5730\u53EC\u56DE\uFF0C\u7136\u540E\u8981\u6C42\u6A21\u578B\u7ED9\u6BCF\u5F20\u5019\u9009\u56FE\u6253 0-100 \u5206\uFF0C\u4F4E\u4E8E\u300C\u6700\u4F4E\u5206\u6570\u7EBF\u300D\u7684\u4E00\u5F8B\u4E0D\u8981\u3002\n \u4E00\u5F20\u90FD\u4E0D\u8FBE\u6807\u5C31\u5982\u5B9E\u544A\u77E5\u7528\u6237\u300C\u56FE\u5E93\u91CC\u6CA1\u6709\u5408\u9002\u7684\u53C2\u8003\u56FE\u300D\uFF0C**\u4E0D\u4F1A\u786C\u51D1**\uFF1B\n \u82E5\u5F00\u542F\u300C\u7F3A\u5C11\u5408\u9002\u53C2\u8003\u56FE\u65F6\u8BE2\u95EE\u7528\u6237\u8865\u5145\u53D1\u9001\u300D\uFF0C\u4F1A\u8BF7\u7528\u6237\u8865\u53D1\u4E00\u5F20\u53C2\u8003\u56FE\uFF08\u7528\u6237\u53D1\u7684\u56FE\u4F1A\u88AB\u76F4\u63A5\u4F7F\u7528\uFF09\u3002\n \u60F3\u56DE\u5230\u8001\u884C\u4E3A\u5C31\u628A\u300C\u9009\u56FE\u6A21\u5F0F\u300D\u6539\u6210\u300C\u65E7\u7248\u300D\u3002\n5. \u5F53\u7528\u6237\u660E\u786E\u8BF4\u300C\u4E0D\u7528\u53C2\u8003 / \u968F\u610F\u753B / \u81EA\u7531\u53D1\u6325\u300D\u65F6\uFF0C\u76F4\u63A5\u8DF3\u8FC7\u9009\u56FE\uFF0C\u4E00\u6B21\u9009\u56FE\u8BF7\u6C42\u90FD\u4E0D\u53D1\u3002\n6. \u6307\u4EE4\u4E00\u89E6\u53D1\u5C31\u5148\u56DE\u4E00\u6761\u300C\u6536\u5230\uFF0C\u6B63\u5728\u51C6\u5907...\u300D\uFF08\u300C\u6D88\u606F\u53D1\u9001 \u2192 \u6536\u5230\u63D0\u793A\u300D\uFF0C\u9ED8\u8BA4\u5F00\uFF09\u3002\n \u540E\u9762\u7684\u4F18\u5316\u63D0\u793A\u8BCD + \u9009\u56FE\u662F\u4E24\u8F6E\u6A21\u578B\u8BF7\u6C42\uFF0C\u53EF\u80FD\u8981\u51E0\u5341\u79D2\uFF0C\u4E0D\u53D1\u8FD9\u6761\u7528\u6237\u4F1A\u4EE5\u4E3A\u673A\u5668\u4EBA\u5361\u6B7B\u3002\n7. **\u5F00\u753B\u524D\u786E\u8BA4**\uFF08\u300C\u6D88\u606F\u53D1\u9001 \u2192 \u5F00\u753B\u524D\u8BF7\u7528\u6237\u786E\u8BA4\u300D\uFF0C\u9ED8\u8BA4\u5F00\uFF09\uFF1AAI \u6311\u5B8C\u53C2\u8003\u56FE\u5148\u4E0D\u753B\uFF0C\n \u628A\u300C\u672C\u6B21\u4F1A\u53C2\u8003\u54EA\u51E0\u5F20\u56FE\u300D\u5217\u51FA\u6765\uFF0C\u56DE\u590D\u300C\u786E\u8BA4\u300D\u624D\u5F00\u753B\uFF08\u8D85\u65F6\u6216\u56DE\u522B\u7684\u90FD\u7B97\u53D6\u6D88\uFF09\u3002\n \u4E0D\u60F3\u591A\u8FD9\u4E00\u6B65\u5C31\u5173\u6389\uFF0C\u6216\u5728\u5355\u4E2A\u6307\u4EE4\u4E0A\u8BBE\u6210\u300C\u9009\u597D\u56FE\u76F4\u63A5\u5F00\u753B\u300D\u3002\n8. \u300C\u542F\u7528 AI \u667A\u80FD\u9009\u62E9\u53C2\u8003\u56FE\u7247\u300D\u9ED8\u8BA4\u5173\u95ED\uFF0C\u9700\u624B\u52A8\u5F00\u542F\uFF1B\u300CAI \u9009\u56FE\u5931\u8D25\u65F6\u56DE\u9000\u4E3A\u4F7F\u7528\u5019\u9009\u6C60\u5185\u5168\u90E8\u56FE\u7247\u300D\u9ED8\u8BA4\u5173\u95ED\uFF0C\n \u5931\u8D25\u65F6\u672C\u6B21\u4E0D\u4F7F\u7528\u53C2\u8003\u56FE\u7247\uFF08\u5F00\u542F\u5219\u6539\u7528\u5019\u9009\u6C60\u5185\u5168\u90E8\u56FE\u7247\uFF09\u3002\u6CE8\u610F\u300C\u56DE\u9000\u300D\u53EA\u5BF9**\u8BF7\u6C42\u5931\u8D25**\u751F\u6548\uFF0C\n \u5BF9\u300C\u6253\u5206\u4E0D\u8FBE\u6807\u300D\u4E0D\u751F\u6548 \u2014\u2014 \u540E\u8005\u5C31\u8BE5\u4E0D\u5E26\u53C2\u8003\u56FE\u3002\n9. \u300C\u65E7\u7248\u300D\u6A21\u5F0F\u4E0B\u53C2\u8003\u56FE\u8F83\u591A\u65F6\uFF08\u8D85\u8FC7 12 \u5F20\uFF09\u81EA\u52A8\u8D70\u4E24\u7EA7\u68C0\u7D22\uFF1A\u5148\u8BA9\u6A21\u578B\u4EA7\u51FA\u68C0\u7D22\u5173\u952E\u8BCD\u3001\u672C\u5730\u5339\u914D\u53EC\u56DE\uFF0C\n \u518D\u5BF9\u53EC\u56DE\u7ED3\u679C\u7CBE\u6392\uFF1B\u8BC6\u56FE\u6A21\u5F0F\u4E0B\u4E5F\u53EA\u53D1\u9001\u53EC\u56DE\u7684\u8FD9\u51E0\u5F20\u56FE\u7247\uFF0C\u907F\u514D\u6BCF\u6B21\u90FD\u628A\u6574\u4E2A\u56FE\u5E93\u53D1\u7ED9\u6A21\u578B\u3002\n10. \u63CF\u8FF0\u53EF\u4EE5\u7528\u300C\u751F\u6210\u63CF\u8FF0\u300D\u6307\u4EE4\u8BA9\u6A21\u578B\u770B\u56FE\u81EA\u52A8\u751F\u6210\u5E76\u5199\u56DE\u914D\u7F6E\uFF08\u9700\u8981\u652F\u6301\u56FE\u7247\u8F93\u5165\u7684\u6A21\u578B\uFF09\u3002\n \u82E5\u9009\u56FE\u6A21\u578B\u652F\u6301\u8BC6\u522B\u56FE\u7247\uFF08\u591A\u6A21\u6001\uFF09\uFF0C\u53EF\u52FE\u9009\u300C\u9009\u56FE\u6A21\u578B\u652F\u6301\u8BC6\u522B\u56FE\u7247\u300D\uFF0C\u63D2\u4EF6\u4F1A\u628A\u5019\u9009\u56FE\u7247**\u81EA\u5DF1\u4E0B\u8F7D\u540E\u8F6C base64**\n \u518D\u53D1\u7ED9\u6A21\u578B\uFF08\u76F4\u63A5\u4E22\u94FE\u63A5\u7684\u8BDD\uFF0C\u6A21\u578B\u670D\u52A1\u7AEF\u62C9\u4E0D\u5230\u5C31\u4F1A\u6574\u6761\u8BF7\u6C42 400\uFF09\uFF0C\u6A21\u578B\u5BF9\u7740\u771F\u5B9E\u56FE\u7247\u6311\u9009\uFF1B\n \u5B83\u7ED9\u51FA\u7684\u5173\u952E\u89C6\u89C9\u7279\u5F81\u8FD8\u4F1A\u5E76\u5165\u7ED8\u56FE\u63D0\u793A\u8BCD\uFF0C\u8BA9\u51FA\u56FE\u66F4\u8FD8\u539F\u53C2\u8003\u56FE\u3002\n11. \u751F\u6210\u7ED3\u679C\u53EF\u81EA\u52A8\u5165\u5E93\uFF08\u9ED8\u8BA4\u5173\u95ED\uFF09\uFF0C\u4E0B\u6B21\u80FD\u88AB\u81EA\u5DF1\u68C0\u7D22\u5230\u5E76\u590D\u7528\uFF0C\u5F62\u6210\u95ED\u73AF\uFF1B\n \u5F00\u542F\u300C\u540E\u53F0\u7ED8\u56FE\u300D\u540E\u51FA\u56FE\u4E0D\u518D\u963B\u585E\uFF0C\u5148\u56DE\u300C\u6B63\u5728\u753B\u300D\uFF0C\u753B\u597D\u4E3B\u52A8\u63A8\u9001\u3002\n\n\u63D0\u793A\u8BCD\u6A21\u677F\u53EF\u7528\u5360\u4F4D\u7B26\uFF1A{candidates} \u5019\u9009\u56FE\u7247\u5217\u8868\u3001{userInput} \u7528\u6237\u9644\u52A0\u9700\u6C42\u3001{max} \u6700\u591A\u9009\u62E9\u6570\u91CF\u3001{command} \u6307\u4EE4\u540D\u3001{prompt} \u6307\u4EE4\u63D0\u793A\u8BCD\n\n---\n\u6B64\u9879\u76EE\u6240\u9700\u7684koishi\u670D\u52A1\uFF1A\u5FC5\u9700 'http', 'logger', 'i18n'\uFF1B\u53EF\u9009 'puppeteer'\uFF08\u4EC5\u300C\u6587\u5B57\u6E32\u67D3\u53C2\u8003\u56FE\u300D\u9700\u8981\uFF09\n\n---\n";
13
13
  /**
14
14
  * 解析模型返回的 JSON 选择结果(纯函数,便于单独测试)
15
15
  * @param raw 模型的原始回复
@@ -62,6 +62,16 @@ export declare function extractJsonObject(raw: string | null | undefined): any |
62
62
  export declare function toStrList(value: any): string[];
63
63
  /** 用户这句话是不是「不用参考了」 */
64
64
  export declare function isSkipReferenceInput(userInput: string): boolean;
65
+ /**
66
+ * 兜住「需求分析」模型爱乱加的废话。
67
+ *
68
+ * 实测模型会把 needTypes 写成
69
+ * 「能发一张你想用的自己形象的参考图吗?顺便说说 DeepSeek 画成鲸鱼可以吗?」——
70
+ * 后半句是凭空瞎聊,用户看了莫名其妙。这里只保留第一句、去掉换行和 markdown 修饰、
71
+ * 限制长度,把「询问补图」这件事还原成一句话。
72
+ */
73
+ export declare function sanitizeAskMessage(raw: string | null | undefined, max?: number): string;
74
+ export declare function isConfirmInput(raw: string | null | undefined): boolean;
65
75
  /** 解析「需求抽取」的返回 */
66
76
  export declare function parseAnalysisResult(raw: string | null): RequirementAnalysis | null;
67
77
  /** 把「需求要素」转成本地检索用的关键词 */
@@ -194,8 +204,11 @@ export declare function withTokenParam(body: any, param: string): any;
194
204
  * @param candidates 候选图片
195
205
  * @param vision 是否附带图片
196
206
  * @param visionMaxImages 最多附带多少张图片
207
+ * @param dataUrls 已经自己下载好的 base64 data URL(按 candidates 下标)。**传了它之后,
208
+ * 没下载成功的图就不再发原始链接了** —— 上游拉不到那个链接会整条请求 400,
209
+ * 连文字描述一起废掉;只发描述反而还能用。
197
210
  */
198
- export declare function buildSelectorContent(text: string, candidates: CandidateImage[], vision: boolean, visionMaxImages: number): string | any[];
211
+ export declare function buildSelectorContent(text: string, candidates: CandidateImage[], vision: boolean, visionMaxImages: number, dataUrls?: (string | undefined)[]): string | any[];
199
212
  /** HTML 转义,避免渲染时把文字当标签吃掉 */
200
213
  export declare function escapeHtml(text: string): string;
201
214
  /** 把要渲染的文字拼成一份 HTML(供无头浏览器截图) */
@@ -223,6 +236,8 @@ interface CommandConfig {
223
236
  aiSelect?: boolean;
224
237
  aiMaxSelect?: number;
225
238
  aiAskUser?: boolean;
239
+ /** 该指令是否在开画前请用户确认:'follow' 跟随全局 / 'on' 总是确认 / 'off' 从不确认 */
240
+ confirmBeforeDraw?: 'follow' | 'on' | 'off' | boolean;
226
241
  }[];
227
242
  };
228
243
  defaultWaitTimeout: number;
@@ -244,6 +259,12 @@ interface CommandConfig {
244
259
  textRender?: TextRenderConfig;
245
260
  /** 把处理过程中的多条提示合并成一条消息发出(省被动消息额度) */
246
261
  mergeNotifications?: boolean;
262
+ /** 指令一触发就立刻回一条「收到」,避免用户以为卡住 */
263
+ ackOnStart?: boolean;
264
+ /** AI 选好参考图后先请用户确认再开画 */
265
+ confirmBeforeDraw?: boolean;
266
+ /** 等待用户确认的时间(秒) */
267
+ confirmTimeout?: number;
247
268
  /** 结果图用 markdown 的 ![](url) 单独发一条(支持的平台) */
248
269
  markdownImage?: boolean;
249
270
  /** 结果图先经 assets 服务上传再发(外链在手机端 QQ 可能拉不到) */
@@ -340,6 +361,8 @@ interface AISelectorConfig {
340
361
  notify: boolean;
341
362
  vision: boolean;
342
363
  visionMaxImages: number;
364
+ /** 识图时单张候选图的体积上限(MB),超过就只发文字描述 */
365
+ visionMaxImageBytes?: number;
343
366
  visionFallback: boolean;
344
367
  appendHint: boolean;
345
368
  twoStage: boolean;
package/lib/index.js CHANGED
@@ -46,6 +46,7 @@ __export(src_exports, {
46
46
  extractTextToRender: () => extractTextToRender,
47
47
  fitImageSize: () => fitImageSize,
48
48
  inject: () => inject,
49
+ isConfirmInput: () => isConfirmInput,
49
50
  isPassiveReplyError: () => isPassiveReplyError,
50
51
  isRetryableStatus: () => isRetryableStatus,
51
52
  isSkipReferenceInput: () => isSkipReferenceInput,
@@ -62,6 +63,7 @@ __export(src_exports, {
62
63
  previewJson: () => previewJson,
63
64
  readImageSize: () => readImageSize,
64
65
  resolveTokenParam: () => resolveTokenParam,
66
+ sanitizeAskMessage: () => sanitizeAskMessage,
65
67
  sanitizeCaption: () => sanitizeCaption,
66
68
  selectByScore: () => selectByScore,
67
69
  stripCommandName: () => stripCommandName,
@@ -106,15 +108,21 @@ var usage = `
106
108
  \u82E5\u5F00\u542F\u300C\u7F3A\u5C11\u5408\u9002\u53C2\u8003\u56FE\u65F6\u8BE2\u95EE\u7528\u6237\u8865\u5145\u53D1\u9001\u300D\uFF0C\u4F1A\u8BF7\u7528\u6237\u8865\u53D1\u4E00\u5F20\u53C2\u8003\u56FE\uFF08\u7528\u6237\u53D1\u7684\u56FE\u4F1A\u88AB\u76F4\u63A5\u4F7F\u7528\uFF09\u3002
107
109
  \u60F3\u56DE\u5230\u8001\u884C\u4E3A\u5C31\u628A\u300C\u9009\u56FE\u6A21\u5F0F\u300D\u6539\u6210\u300C\u65E7\u7248\u300D\u3002
108
110
  5. \u5F53\u7528\u6237\u660E\u786E\u8BF4\u300C\u4E0D\u7528\u53C2\u8003 / \u968F\u610F\u753B / \u81EA\u7531\u53D1\u6325\u300D\u65F6\uFF0C\u76F4\u63A5\u8DF3\u8FC7\u9009\u56FE\uFF0C\u4E00\u6B21\u9009\u56FE\u8BF7\u6C42\u90FD\u4E0D\u53D1\u3002
109
- 6. \u300C\u542F\u7528 AI \u667A\u80FD\u9009\u62E9\u53C2\u8003\u56FE\u7247\u300D\u9ED8\u8BA4\u5173\u95ED\uFF0C\u9700\u624B\u52A8\u5F00\u542F\uFF1B\u300CAI \u9009\u56FE\u5931\u8D25\u65F6\u56DE\u9000\u4E3A\u4F7F\u7528\u5019\u9009\u6C60\u5185\u5168\u90E8\u56FE\u7247\u300D\u9ED8\u8BA4\u5173\u95ED\uFF0C
111
+ 6. \u6307\u4EE4\u4E00\u89E6\u53D1\u5C31\u5148\u56DE\u4E00\u6761\u300C\u6536\u5230\uFF0C\u6B63\u5728\u51C6\u5907...\u300D\uFF08\u300C\u6D88\u606F\u53D1\u9001 \u2192 \u6536\u5230\u63D0\u793A\u300D\uFF0C\u9ED8\u8BA4\u5F00\uFF09\u3002
112
+ \u540E\u9762\u7684\u4F18\u5316\u63D0\u793A\u8BCD + \u9009\u56FE\u662F\u4E24\u8F6E\u6A21\u578B\u8BF7\u6C42\uFF0C\u53EF\u80FD\u8981\u51E0\u5341\u79D2\uFF0C\u4E0D\u53D1\u8FD9\u6761\u7528\u6237\u4F1A\u4EE5\u4E3A\u673A\u5668\u4EBA\u5361\u6B7B\u3002
113
+ 7. **\u5F00\u753B\u524D\u786E\u8BA4**\uFF08\u300C\u6D88\u606F\u53D1\u9001 \u2192 \u5F00\u753B\u524D\u8BF7\u7528\u6237\u786E\u8BA4\u300D\uFF0C\u9ED8\u8BA4\u5F00\uFF09\uFF1AAI \u6311\u5B8C\u53C2\u8003\u56FE\u5148\u4E0D\u753B\uFF0C
114
+ \u628A\u300C\u672C\u6B21\u4F1A\u53C2\u8003\u54EA\u51E0\u5F20\u56FE\u300D\u5217\u51FA\u6765\uFF0C\u56DE\u590D\u300C\u786E\u8BA4\u300D\u624D\u5F00\u753B\uFF08\u8D85\u65F6\u6216\u56DE\u522B\u7684\u90FD\u7B97\u53D6\u6D88\uFF09\u3002
115
+ \u4E0D\u60F3\u591A\u8FD9\u4E00\u6B65\u5C31\u5173\u6389\uFF0C\u6216\u5728\u5355\u4E2A\u6307\u4EE4\u4E0A\u8BBE\u6210\u300C\u9009\u597D\u56FE\u76F4\u63A5\u5F00\u753B\u300D\u3002
116
+ 8. \u300C\u542F\u7528 AI \u667A\u80FD\u9009\u62E9\u53C2\u8003\u56FE\u7247\u300D\u9ED8\u8BA4\u5173\u95ED\uFF0C\u9700\u624B\u52A8\u5F00\u542F\uFF1B\u300CAI \u9009\u56FE\u5931\u8D25\u65F6\u56DE\u9000\u4E3A\u4F7F\u7528\u5019\u9009\u6C60\u5185\u5168\u90E8\u56FE\u7247\u300D\u9ED8\u8BA4\u5173\u95ED\uFF0C
110
117
  \u5931\u8D25\u65F6\u672C\u6B21\u4E0D\u4F7F\u7528\u53C2\u8003\u56FE\u7247\uFF08\u5F00\u542F\u5219\u6539\u7528\u5019\u9009\u6C60\u5185\u5168\u90E8\u56FE\u7247\uFF09\u3002\u6CE8\u610F\u300C\u56DE\u9000\u300D\u53EA\u5BF9**\u8BF7\u6C42\u5931\u8D25**\u751F\u6548\uFF0C
111
118
  \u5BF9\u300C\u6253\u5206\u4E0D\u8FBE\u6807\u300D\u4E0D\u751F\u6548 \u2014\u2014 \u540E\u8005\u5C31\u8BE5\u4E0D\u5E26\u53C2\u8003\u56FE\u3002
112
- 7. \u300C\u65E7\u7248\u300D\u6A21\u5F0F\u4E0B\u53C2\u8003\u56FE\u8F83\u591A\u65F6\uFF08\u8D85\u8FC7 12 \u5F20\uFF09\u81EA\u52A8\u8D70\u4E24\u7EA7\u68C0\u7D22\uFF1A\u5148\u8BA9\u6A21\u578B\u4EA7\u51FA\u68C0\u7D22\u5173\u952E\u8BCD\u3001\u672C\u5730\u5339\u914D\u53EC\u56DE\uFF0C
119
+ 9. \u300C\u65E7\u7248\u300D\u6A21\u5F0F\u4E0B\u53C2\u8003\u56FE\u8F83\u591A\u65F6\uFF08\u8D85\u8FC7 12 \u5F20\uFF09\u81EA\u52A8\u8D70\u4E24\u7EA7\u68C0\u7D22\uFF1A\u5148\u8BA9\u6A21\u578B\u4EA7\u51FA\u68C0\u7D22\u5173\u952E\u8BCD\u3001\u672C\u5730\u5339\u914D\u53EC\u56DE\uFF0C
113
120
  \u518D\u5BF9\u53EC\u56DE\u7ED3\u679C\u7CBE\u6392\uFF1B\u8BC6\u56FE\u6A21\u5F0F\u4E0B\u4E5F\u53EA\u53D1\u9001\u53EC\u56DE\u7684\u8FD9\u51E0\u5F20\u56FE\u7247\uFF0C\u907F\u514D\u6BCF\u6B21\u90FD\u628A\u6574\u4E2A\u56FE\u5E93\u53D1\u7ED9\u6A21\u578B\u3002
114
- 8. \u63CF\u8FF0\u53EF\u4EE5\u7528\u300C\u751F\u6210\u63CF\u8FF0\u300D\u6307\u4EE4\u8BA9\u6A21\u578B\u770B\u56FE\u81EA\u52A8\u751F\u6210\u5E76\u5199\u56DE\u914D\u7F6E\uFF08\u9700\u8981\u652F\u6301\u56FE\u7247\u8F93\u5165\u7684\u6A21\u578B\uFF09\u3002
115
- \u82E5\u9009\u56FE\u6A21\u578B\u652F\u6301\u8BC6\u522B\u56FE\u7247\uFF08\u591A\u6A21\u6001\uFF09\uFF0C\u53EF\u52FE\u9009\u300C\u9009\u56FE\u6A21\u578B\u652F\u6301\u8BC6\u522B\u56FE\u7247\u300D\uFF0C\u63D2\u4EF6\u4F1A\u628A\u5019\u9009\u56FE\u7247\u672C\u8EAB\u53D1\u7ED9\u6A21\u578B\uFF0C
116
- \u6A21\u578B\u5BF9\u7740\u771F\u5B9E\u56FE\u7247\u6311\u9009\uFF1B\u5B83\u7ED9\u51FA\u7684\u5173\u952E\u89C6\u89C9\u7279\u5F81\u8FD8\u4F1A\u5E76\u5165\u7ED8\u56FE\u63D0\u793A\u8BCD\uFF0C\u8BA9\u51FA\u56FE\u66F4\u8FD8\u539F\u53C2\u8003\u56FE\u3002
117
- 9. \u751F\u6210\u7ED3\u679C\u53EF\u81EA\u52A8\u5165\u5E93\uFF08\u9ED8\u8BA4\u5173\u95ED\uFF09\uFF0C\u4E0B\u6B21\u80FD\u88AB\u81EA\u5DF1\u68C0\u7D22\u5230\u5E76\u590D\u7528\uFF0C\u5F62\u6210\u95ED\u73AF\uFF1B
121
+ 10. \u63CF\u8FF0\u53EF\u4EE5\u7528\u300C\u751F\u6210\u63CF\u8FF0\u300D\u6307\u4EE4\u8BA9\u6A21\u578B\u770B\u56FE\u81EA\u52A8\u751F\u6210\u5E76\u5199\u56DE\u914D\u7F6E\uFF08\u9700\u8981\u652F\u6301\u56FE\u7247\u8F93\u5165\u7684\u6A21\u578B\uFF09\u3002
122
+ \u82E5\u9009\u56FE\u6A21\u578B\u652F\u6301\u8BC6\u522B\u56FE\u7247\uFF08\u591A\u6A21\u6001\uFF09\uFF0C\u53EF\u52FE\u9009\u300C\u9009\u56FE\u6A21\u578B\u652F\u6301\u8BC6\u522B\u56FE\u7247\u300D\uFF0C\u63D2\u4EF6\u4F1A\u628A\u5019\u9009\u56FE\u7247**\u81EA\u5DF1\u4E0B\u8F7D\u540E\u8F6C base64**
123
+ \u518D\u53D1\u7ED9\u6A21\u578B\uFF08\u76F4\u63A5\u4E22\u94FE\u63A5\u7684\u8BDD\uFF0C\u6A21\u578B\u670D\u52A1\u7AEF\u62C9\u4E0D\u5230\u5C31\u4F1A\u6574\u6761\u8BF7\u6C42 400\uFF09\uFF0C\u6A21\u578B\u5BF9\u7740\u771F\u5B9E\u56FE\u7247\u6311\u9009\uFF1B
124
+ \u5B83\u7ED9\u51FA\u7684\u5173\u952E\u89C6\u89C9\u7279\u5F81\u8FD8\u4F1A\u5E76\u5165\u7ED8\u56FE\u63D0\u793A\u8BCD\uFF0C\u8BA9\u51FA\u56FE\u66F4\u8FD8\u539F\u53C2\u8003\u56FE\u3002
125
+ 11. \u751F\u6210\u7ED3\u679C\u53EF\u81EA\u52A8\u5165\u5E93\uFF08\u9ED8\u8BA4\u5173\u95ED\uFF09\uFF0C\u4E0B\u6B21\u80FD\u88AB\u81EA\u5DF1\u68C0\u7D22\u5230\u5E76\u590D\u7528\uFF0C\u5F62\u6210\u95ED\u73AF\uFF1B
118
126
  \u5F00\u542F\u300C\u540E\u53F0\u7ED8\u56FE\u300D\u540E\u51FA\u56FE\u4E0D\u518D\u963B\u585E\uFF0C\u5148\u56DE\u300C\u6B63\u5728\u753B\u300D\uFF0C\u753B\u597D\u4E3B\u52A8\u63A8\u9001\u3002
119
127
 
120
128
  \u63D0\u793A\u8BCD\u6A21\u677F\u53EF\u7528\u5360\u4F4D\u7B26\uFF1A{candidates} \u5019\u9009\u56FE\u7247\u5217\u8868\u3001{userInput} \u7528\u6237\u9644\u52A0\u9700\u6C42\u3001{max} \u6700\u591A\u9009\u62E9\u6570\u91CF\u3001{command} \u6307\u4EE4\u540D\u3001{prompt} \u6307\u4EE4\u63D0\u793A\u8BCD
@@ -261,6 +269,21 @@ function isSkipReferenceInput(userInput) {
261
269
  if (!text) return false;
262
270
  return SKIP_REFERENCE_RE.test(text);
263
271
  }
272
+ function sanitizeAskMessage(raw, max = 40) {
273
+ let text = String(raw ?? "").replace(/\s+/g, " ").trim();
274
+ if (!text) return "";
275
+ const first = text.match(/^[^。!?!?;;]*[。!?!?;;]?/);
276
+ if (first) text = first[0].trim();
277
+ text = text.replace(/^[-*>#\s]+/, "").replace(/^["'“”「」『』]+/, "").replace(/["'“”「」『』]+$/, "").trim();
278
+ if (text.length > max) text = text.slice(0, max) + "\u2026";
279
+ return text;
280
+ }
281
+ var CONFIRM_RE = /^(确认|确定|可以|好的|好|行|嗯|是|是的|开始|开画|画吧|继续|ok|okay|yes|y|1|\+1)$/i;
282
+ function isConfirmInput(raw) {
283
+ const text = String(raw ?? "").replace(/[\s\u3000]+/g, "").replace(/[!!。.,,~~??::]+$/g, "");
284
+ if (!text || text.length > 12) return false;
285
+ return CONFIRM_RE.test(text);
286
+ }
264
287
  function parseAnalysisResult(raw) {
265
288
  const data = extractJsonObject(raw);
266
289
  if (!data) return null;
@@ -272,7 +295,7 @@ function parseAnalysisResult(raw) {
272
295
  avoid: toStrList(data.avoid ?? data.exclude ?? data.negative ?? data.\u4E0D\u8981\u51FA\u73B0),
273
296
  text: Array.isArray(text) ? toStrList(text).join("\n") : String(text ?? "").trim(),
274
297
  skipReference: data.skipReference === true || data.skip_reference === true,
275
- needTypes: String(data.needTypes ?? data.need_types ?? data.\u9700\u8981\u7684\u56FE ?? "").trim()
298
+ needTypes: sanitizeAskMessage(data.needTypes ?? data.need_types ?? data.\u9700\u8981\u7684\u56FE ?? "")
276
299
  };
277
300
  }
278
301
  function keywordsFromAnalysis(analysis) {
@@ -700,14 +723,15 @@ function withTokenParam(body, param) {
700
723
  if (value !== void 0) next[param] = value;
701
724
  return next;
702
725
  }
703
- function buildSelectorContent(text, candidates, vision, visionMaxImages) {
726
+ function buildSelectorContent(text, candidates, vision, visionMaxImages, dataUrls) {
704
727
  if (!vision) return text;
705
728
  const max = visionMaxImages > 0 ? visionMaxImages : candidates.length;
706
729
  const parts = [];
707
730
  candidates.forEach((candidate, index) => {
708
731
  parts.push({ type: "text", text: `[${index + 1}] \u6240\u5C5E\u7EC4\uFF1A${candidate.group} | \u63CF\u8FF0\uFF1A${candidate.description || "\uFF08\u65E0\u63CF\u8FF0\uFF09"}` });
709
732
  if (index < max) {
710
- parts.push({ type: "image_url", image_url: { url: candidate.url } });
733
+ const url = dataUrls ? dataUrls[index] : candidate.url;
734
+ if (url) parts.push({ type: "image_url", image_url: { url } });
711
735
  }
712
736
  });
713
737
  const hintRule = visionMaxImages && candidates.length > 1 ? `\u5E76\u5728 hint \u5B57\u6BB5\u4E2D\u5199\u4E00\u6BB5\u300C\u753B\u9762\u7F16\u6392\u300D\uFF1A\u6309\u9009\u4E2D\u987A\u5E8F\u8BF4\u660E\u6BCF\u4E2A\u89D2\u8272/\u5143\u7D20\u5728\u753B\u9762\u4E2D\u7684\u4F4D\u7F6E\u548C\u59FF\u6001\u3001\u5F7C\u6B64\u7684\u52A8\u4F5C\u4E92\u52A8\u4E0E\u89C6\u7EBF\u5173\u7CFB\u3001\u5171\u540C\u6240\u5904\u7684\u573A\u666F\u4E0E\u6C1B\u56F4\uFF0C\u4EE5\u53CA\u9700\u8981\u8FD8\u539F\u7684\u5404\u81EA\u5916\u5F62\u7279\u5F81\uFF08\u53D1\u578B\u53D1\u8272\u3001\u670D\u88C5\u3001\u914D\u8272\u3001\u753B\u98CE\uFF09\u3002\u5185\u5BB9\u8981\u50CF\u4E00\u6BB5\u53EF\u4EE5\u76F4\u63A5\u4EA4\u7ED9\u753B\u5E08\u7684\u5206\u955C\u8BF4\u660E\uFF0C\u800C\u4E0D\u662F\u9010\u5F20\u7F57\u5217\u3002` : `\u5E76\u5728 hint \u5B57\u6BB5\u4E2D\u7528\u4E00\u53E5\u8BDD\u6982\u62EC\u4F60\u9009\u4E2D\u56FE\u7247\u7684\u5173\u952E\u89C6\u89C9\u7279\u5F81\uFF08\u5982\u53D1\u578B\u3001\u53D1\u8272\u3001\u670D\u88C5\u3001\u914D\u8272\u3001\u59FF\u6001\u3001\u753B\u98CE\uFF09\uFF0C\u4F9B\u540E\u7EED\u7ED8\u56FE\u65F6\u8FD8\u539F\u8BE5\u53C2\u8003\u56FE\u3002`;
@@ -855,6 +879,7 @@ var DEFAULT_ANALYZE_PROMPT = `\u4F60\u662F\u7ED8\u56FE\u53C2\u8003\u56FE\u7684\u
855
879
  6. skipReference\uFF1A\u7528\u6237\u660E\u786E\u8868\u793A\u300C\u4E0D\u7528\u53C2\u8003 / \u968F\u610F\u753B / \u81EA\u7531\u53D1\u6325 / \u4F60\u770B\u7740\u529E\u300D\u65F6\u4E3A true\u3002
856
880
  7. needTypes\uFF1A\u56FE\u5E93\u91CC**\u6CA1\u6709**\u5408\u9002\u53C2\u8003\u56FE\u65F6\uFF0C\u63D2\u4EF6\u8981\u8BF7\u7528\u6237\u8865\u53D1\u4E00\u5F20\u3002\u8FD9\u91CC\u5199\u4E00\u53E5**\u76F4\u63A5\u5BF9\u7528\u6237\u8BF4**\u7684\u8BDD
857
881
  \uFF08\u4E2D\u6587\uFF0C40 \u5B57\u4EE5\u5185\uFF0C\u8BED\u6C14\u81EA\u7136\uFF0C\u4E0D\u8981\u7528\u300C\u8BF7\u63D0\u4F9B\u56FE\u7247\u300D\u8FD9\u79CD\u5957\u8BDD\uFF09\uFF0C\u4F8B\u5982\u300C\u80FD\u53D1\u4E00\u5F20\u767D\u53D1\u6C34\u624B\u670D\u7684\u7ACB\u7ED8\u5417\uFF1F\u300D\u3002
882
+ **\u53EA\u5199\u8FD9\u4E00\u53E5**\uFF1A\u4E0D\u8981\u8FFD\u95EE\u7B2C\u4E8C\u4EF6\u4E8B\u3001\u4E0D\u8981\u95F2\u804A\u3001\u4E0D\u8981\u63D0\u5230\u6A21\u578B\u540D\u5B57\uFF08\u5982 DeepSeek\uFF09\u3001\u4E0D\u8981\u5199\u300C\u987A\u4FBF\u8BF4\u8BF4\u300D\u4E4B\u7C7B\u7684\u8BDD\u3002
858
883
  \u56FE\u5E93\u91CC\u4E0D\u7F3A\u56FE\u5C31\u7559\u7A7A\u5B57\u7B26\u4E32\u3002
859
884
 
860
885
  \u53EA\u8F93\u51FA\u4E00\u4E2A JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981\u89E3\u91CA\u3001\u4E0D\u8981 Markdown \u4EE3\u7801\u5757\uFF1A
@@ -1336,7 +1361,12 @@ var Config = import_koishi.Schema.intersect([
1336
1361
  referenceGroups: import_koishi.Schema.array(import_koishi.Schema.string()).description("\u5F15\u7528\u7684\u53C2\u8003\u56FE\u7247\u7EC4\u540D\u79F0\uFF08AI \u5C06\u4ECE\u8FD9\u4E9B\u7EC4\u4E2D\u6311\u9009\u56FE\u7247\uFF0C\u7559\u7A7A\u5219\u7531\u300CAI \u9009\u56FE\u8BBE\u7F6E\u300D\u51B3\u5B9A\u662F\u5426\u4F7F\u7528\u5168\u90E8\u7EC4\uFF09").default([]),
1337
1362
  aiSelect: import_koishi.Schema.boolean().default(true).description("\u542F\u7528 AI \u667A\u80FD\u9009\u62E9\u53C2\u8003\u56FE\u7247"),
1338
1363
  aiMaxSelect: import_koishi.Schema.number().default(0).min(0).max(10).step(1).description("AI \u6700\u591A\u4E3A\u8BE5\u6307\u4EE4\u9009\u62E9\u7684\u53C2\u8003\u56FE\u7247\u6570\u91CF\uFF080 = \u8DDF\u968F\u300CAI \u9009\u56FE\u8BBE\u7F6E\u300D\u91CC\u7684\u5168\u5C40\u6570\u91CF\uFF09"),
1339
- aiAskUser: import_koishi.Schema.boolean().default(true).description("\u7F3A\u5C11\u5408\u9002\u53C2\u8003\u56FE\u65F6\u8BE2\u95EE\u7528\u6237\u8865\u5145\u53D1\u9001")
1364
+ aiAskUser: import_koishi.Schema.boolean().default(true).description("\u7F3A\u5C11\u5408\u9002\u53C2\u8003\u56FE\u65F6\u8BE2\u95EE\u7528\u6237\u8865\u5145\u53D1\u9001"),
1365
+ confirmBeforeDraw: import_koishi.Schema.union([
1366
+ import_koishi.Schema.const("follow").description("\u8DDF\u968F\u5168\u5C40\u8BBE\u7F6E"),
1367
+ import_koishi.Schema.const("on").description("\u5F00\u753B\u524D\u8BF7\u7528\u6237\u786E\u8BA4"),
1368
+ import_koishi.Schema.const("off").description("\u9009\u597D\u56FE\u76F4\u63A5\u5F00\u753B")
1369
+ ]).default("follow").description("\u8BE5\u6307\u4EE4\u662F\u5426\u5728\u5F00\u753B\u524D\u8BF7\u7528\u6237\u786E\u8BA4")
1340
1370
  })
1341
1371
  ).description("\u6307\u4EE4\u914D\u7F6E").default(defaultCommands)
1342
1372
  }).collapse().description("\u6307\u4EE4\u914D\u7F6E\u9879\u592A\u957F\u5566\uFF0C\u8FD9\u6837\u6298\u53E0\u8D77\u6765\u66F4\u65B9\u4FBF\u54E6~"),
@@ -1412,6 +1442,7 @@ var Config = import_koishi.Schema.intersect([
1412
1442
  notify: import_koishi.Schema.boolean().default(true).description("\u5728\u5904\u7406\u63D0\u793A\u4E2D\u9644\u5E26 AI \u9009\u56FE\u7ED3\u679C"),
1413
1443
  vision: import_koishi.Schema.boolean().default(false).description("\u9009\u56FE\u6A21\u578B\u652F\u6301\u8BC6\u522B\u56FE\u7247\uFF08\u591A\u6A21\u6001/VL\uFF09\u3002\u5F00\u542F\u540E\u4F1A\u628A\u5019\u9009\u56FE\u7247\u672C\u8EAB\u4E00\u8D77\u53D1\u7ED9\u6A21\u578B\uFF0C\u800C\u4E0D\u53EA\u662F\u53D1\u6587\u5B57\u63CF\u8FF0\uFF0C\u9009\u5F97\u66F4\u51C6\uFF08\u9700\u8981\u56FE\u7247\u94FE\u63A5\u80FD\u88AB\u6A21\u578B\u8BBF\u95EE\uFF09"),
1414
1444
  visionMaxImages: import_koishi.Schema.number().default(6).min(1).max(20).step(1).description("\u5F00\u542F\u8BC6\u522B\u56FE\u7247\u65F6\uFF0C\u6700\u591A\u9644\u5E26\u591A\u5C11\u5F20\u5019\u9009\u56FE\u7247\uFF08\u8D85\u51FA\u90E8\u5206\u53EA\u53D1\u6587\u5B57\u63CF\u8FF0\uFF0C\u907F\u514D\u8BF7\u6C42\u8FC7\u5927\uFF09"),
1445
+ visionMaxImageBytes: import_koishi.Schema.number().default(4).min(0).max(32).step(1).description("\u8BC6\u56FE\u65F6\u5355\u5F20\u5019\u9009\u56FE\u7684\u4F53\u79EF\u4E0A\u9650\uFF08MB\uFF09\u3002\u63D2\u4EF6\u4F1A\u5148\u628A\u56FE\u7247\u81EA\u5DF1\u4E0B\u8F7D\u4E0B\u6765\u8F6C\u6210 base64 \u518D\u53D1\u7ED9\u9009\u56FE\u6A21\u578B\uFF08\u4E0A\u6E38\u62C9\u4E0D\u5230\u4F60\u7684\u56FE\u7247\u94FE\u63A5\u65F6\u4F1A\u6574\u6761\u8BF7\u6C42 400\uFF09\uFF0C\u8D85\u8FC7\u4E0A\u9650\u7684\u53EA\u53D1\u6587\u5B57\u63CF\u8FF0"),
1415
1446
  visionFallback: import_koishi.Schema.boolean().default(true).description("\u5E26\u56FE\u7247\u8BF7\u6C42\u5931\u8D25\u65F6\uFF0C\u81EA\u52A8\u9000\u56DE\u7EAF\u6587\u5B57\u518D\u8BD5\u4E00\u6B21"),
1416
1447
  appendHint: import_koishi.Schema.boolean().default(true).description("\u628A\u6A21\u578B\u770B\u56FE\u540E\u7ED9\u51FA\u7684\u5173\u952E\u89C6\u89C9\u7279\u5F81\u5E76\u5165\u7ED8\u56FE\u63D0\u793A\u8BCD\uFF08\u4EC5\u5728\u5F00\u542F\u8BC6\u522B\u56FE\u7247\u65F6\u751F\u6548\uFF0C\u8BA9\u51FA\u56FE\u66F4\u8FD8\u539F\u53C2\u8003\u56FE\uFF09"),
1417
1448
  twoStage: import_koishi.Schema.boolean().default(true).description("\u3010\u4EC5\u65E7\u7248\u6A21\u5F0F\u751F\u6548\u3011\u4E24\u7EA7\u68C0\u7D22\u9009\u56FE\uFF1A\u5148\u8BA9\u6A21\u578B\u4EA7\u51FA\u68C0\u7D22\u5173\u952E\u8BCD\u5E76\u5728\u672C\u5730\u53EC\u56DE\uFF0C\u518D\u5BF9\u53EC\u56DE\u7ED3\u679C\u7CBE\u6392\u3002\u6253\u5206\u5236\u5DF2\u5185\u7F6E\u53EC\u56DE\uFF0C\u65E0\u9700\u6B64\u9879"),
@@ -1477,6 +1508,9 @@ var Config = import_koishi.Schema.intersect([
1477
1508
  }).description("\u6587\u5B57\u6E32\u67D3\u53C2\u8003\u56FE\uFF08\u9632\u5D29\u5B57\uFF09"),
1478
1509
  import_koishi.Schema.object({
1479
1510
  mergeNotifications: import_koishi.Schema.boolean().default(true).description("\u628A\u300C\u9009\u56FE\u7ED3\u679C / \u6B63\u5728\u5904\u7406 / \u4F18\u5316\u540E\u63D0\u793A\u8BCD\u300D\u5408\u5E76\u6210**\u4E00\u6761**\u6D88\u606F\u53D1\u51FA\uFF08QQ \u88AB\u52A8\u56DE\u590D\u6709\u6B21\u6570\u4E0A\u9650\uFF0C\u6D88\u606F\u8D8A\u5C11\u8D8A\u7A33\uFF09"),
1511
+ ackOnStart: import_koishi.Schema.boolean().default(true).description("\u6307\u4EE4\u4E00\u89E6\u53D1\u5C31\u7ACB\u523B\u56DE\u4E00\u6761\u300C\u6536\u5230\uFF0C\u6B63\u5728\u51C6\u5907...\u300D\u3002\u540E\u9762\u7684\u300C\u4F18\u5316\u63D0\u793A\u8BCD + AI \u9009\u56FE\u300D\u662F\u4E24\u8F6E\u6A21\u578B\u8BF7\u6C42\u3001\u53EF\u80FD\u8981\u51E0\u5341\u79D2\uFF0C\u4E0D\u53D1\u8FD9\u6761\u7528\u6237\u4F1A\u4EE5\u4E3A\u673A\u5668\u4EBA\u5361\u4F4F\u4E86"),
1512
+ confirmBeforeDraw: import_koishi.Schema.boolean().default(true).description("AI \u9009\u597D\u53C2\u8003\u56FE\u540E**\u5148\u8BF7\u7528\u6237\u786E\u8BA4\u518D\u5F00\u59CB\u753B**\uFF08\u628A\u300C\u672C\u6B21\u53C2\u8003\u8FD9\u51E0\u5F20\u56FE\u300D\u5217\u51FA\u6765\uFF0C\u56DE\u590D\u300C\u786E\u8BA4\u300D\u624D\u753B\uFF09\u3002\u53EF\u9632\u6B62\u9009\u9519\u53C2\u8003\u56FE\u767D\u753B\u4E00\u5F20"),
1513
+ confirmTimeout: import_koishi.Schema.number().default(60).min(5).max(300).step(5).description("\u7B49\u5F85\u7528\u6237\u786E\u8BA4\u7684\u65F6\u95F4\uFF08\u79D2\uFF09\uFF0C\u8D85\u65F6\u6309\u53D6\u6D88\u5904\u7406\uFF1B0 = \u4E0D\u9650\u65F6"),
1480
1514
  markdownImage: import_koishi.Schema.boolean().default(true).description("\u751F\u6210\u7ED3\u679C\u7528 markdown \u7684 ![](\u56FE\u7247\u94FE\u63A5) \u5355\u72EC\u53D1\u4E00\u6761\uFF1B\u4E0D\u652F\u6301 markdown \u7684\u5E73\u53F0\u81EA\u52A8\u9000\u56DE\u666E\u901A\u56FE\u7247\u6D88\u606F"),
1481
1515
  imageViaAssets: import_koishi.Schema.boolean().default(true).description("\u7ED3\u679C\u56FE\u5148\u7ECF assets \u670D\u52A1\u4E0A\u4F20\u518D\u53D1\u3002\u5916\u94FE\u5728\u624B\u673A\u7AEF QQ \u5E38\u5E38\u62C9\u4E0D\u5230\uFF0C\u88C5\u4E0A koishi-plugin-assets-qqbot-part-file \u4E4B\u7C7B\u7684 assets \u63D2\u4EF6\u540E\u7531\u5B83\u8F6C\u6210\u5E73\u53F0\u53EF\u8BBF\u95EE\u7684\u5730\u5740\uFF1B\u4E0A\u4F20\u5931\u8D25\u4F1A\u81EA\u52A8\u7528\u539F\u94FE\u63A5"),
1482
1516
  autoImageSize: import_koishi.Schema.boolean().default(true).description("\u81EA\u52A8\u6309**\u56FE\u7247\u771F\u5B9E\u6BD4\u4F8B**\u751F\u6210 markdown \u5C3A\u5BF8\uFF08\u586B\u6B7B\u5BBD\u9AD8\u4F1A\u628A\u975E\u65B9\u5F62\u56FE\u62C9\u4F38\u53D8\u5F62\uFF09"),
@@ -1578,7 +1612,23 @@ function apply(ctx, config) {
1578
1612
  ctx.logger.warn(`markdown \u56FE\u7247\u53D1\u9001\u5931\u8D25\uFF0C\u9000\u56DE\u666E\u901A\u56FE\u7247\u6D88\u606F: ${error}`);
1579
1613
  }
1580
1614
  }
1581
- await reply(session, [import_koishi.h.image(finalUrl)]);
1615
+ await reply(session, [await buildResultImagePart(finalUrl)]);
1616
+ }
1617
+ async function buildResultImagePart(url) {
1618
+ try {
1619
+ const file = await ctx.http.file(url);
1620
+ const data = file?.data;
1621
+ if (data) {
1622
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
1623
+ if (buffer.length) {
1624
+ logInfo(`\u7ED3\u679C\u56FE\u5DF2\u4E0B\u8F7D\uFF08${(buffer.length / 1024).toFixed(0)}kB\uFF09\uFF0C\u6309\u5B57\u8282\u53D1\u9001`);
1625
+ return import_koishi.h.image(buffer, file?.mime || "image/png");
1626
+ }
1627
+ }
1628
+ } catch (error) {
1629
+ ctx.logger.warn(`\u7ED3\u679C\u56FE\u4E0B\u8F7D\u5931\u8D25\uFF0C\u6539\u53D1\u539F\u59CB\u94FE\u63A5: ${error}`);
1630
+ }
1631
+ return import_koishi.h.image(url);
1582
1632
  }
1583
1633
  async function sendBotMessage(bot, channelId, parts, guildId, messageId) {
1584
1634
  const body = parts.filter(Boolean);
@@ -1603,6 +1653,10 @@ function apply(ctx, config) {
1603
1653
  customprompt: "\u8BF7\u5728{0}\u79D2\u5185\u8F93\u5165\u81EA\u5B9A\u4E49\u63D0\u793A\u8BCD...",
1604
1654
  invalidimage: "\u672A\u68C0\u6D4B\u5230\u6709\u6548\u7684\u56FE\u7247\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u5E26\u56FE\u7247\u7684\u6D88\u606F",
1605
1655
  processing: "\u6B63\u5728\u5904\u7406\u56FE\u7247\uFF0C\u8BF7\u7A0D\u5019...",
1656
+ ack: "\u6536\u5230\uFF0C\u6B63\u5728\u51C6\u5907...",
1657
+ confirmask: "\u51C6\u5907\u5F00\u59CB\u7ED8\u56FE\uFF0C\u672C\u6B21\u4F1A\u53C2\u8003\uFF1A\n{0}\n\n\u56DE\u590D\u300C\u786E\u8BA4\u300D\u5F00\u59CB\u753B\uFF08{1} \u79D2\u5185\u672A\u786E\u8BA4\u5219\u53D6\u6D88\uFF09",
1658
+ confirmcancel: "\u672A\u6536\u5230\u786E\u8BA4\uFF0C\u5DF2\u53D6\u6D88\u672C\u6B21\u7ED8\u56FE",
1659
+ confirmok: "\u5DF2\u786E\u8BA4\uFF0C\u5F00\u59CB\u7ED8\u56FE...",
1606
1660
  failed: "\u56FE\u7247\u751F\u6210\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
1607
1661
  error: "\u5904\u7406\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
1608
1662
  needprompt: "\u8BF7\u63D0\u4F9B\u81EA\u5B9A\u4E49\u63D0\u793A\u8BCD",
@@ -1660,6 +1714,13 @@ function apply(ctx, config) {
1660
1714
  userInputText = stripCommandName(extractTextFromMessage(session.stripped.content), cmdConfig.name);
1661
1715
  }
1662
1716
  logInfo2(`\u7528\u6237\u9644\u52A0\u9700\u6C42: ${userInputText || "\uFF08\u65E0\uFF09"}`);
1717
+ if (config.ackOnStart !== false) {
1718
+ try {
1719
+ await reply(session, [session.text("image-prompt.messages.ack")]);
1720
+ } catch (error) {
1721
+ ctx.logger.warn(`\u53D1\u9001\u300C\u6536\u5230\u300D\u63D0\u793A\u5931\u8D25: ${error}`);
1722
+ }
1723
+ }
1663
1724
  const noticeLines = [];
1664
1725
  const notify = (text) => {
1665
1726
  const value = String(text ?? "").trim();
@@ -1724,6 +1785,7 @@ function apply(ctx, config) {
1724
1785
  let selectionNote = "";
1725
1786
  let aiProvidedImages = 0;
1726
1787
  let aiRenderText = "";
1788
+ const refDescs = [];
1727
1789
  if (aiSelectOn) {
1728
1790
  const candidates = collectCandidates(cmdConfig, defaultImageUrls);
1729
1791
  const declaredGroups = (cmdConfig.referenceGroups || []).filter(Boolean);
@@ -1768,6 +1830,7 @@ function apply(ctx, config) {
1768
1830
  if (selection.ok && selection.picked.length > 0) {
1769
1831
  images.push(...selection.picked.map((c) => c.url));
1770
1832
  aiProvidedImages += selection.picked.length;
1833
+ for (const c of selection.picked) refDescs.push(`\u3010\u56FE\u5E93\u3011${c.description || c.url}`);
1771
1834
  logInfo2(`AI \u9009\u4E2D\u53C2\u8003\u56FE\u7247: ${selection.picked.map((c) => c.url).join(" , ")} \u7406\u7531: ${selection.reason}`);
1772
1835
  if (selector.vision === true && selector.appendHint !== false && selection.hint) {
1773
1836
  const hintKey = selection.picked.length > 1 ? "hintscene" : "hintsingle";
@@ -1830,6 +1893,7 @@ ${session.text(`image-prompt.messages.${hintKey}`, [selection.hint])}`;
1830
1893
  const sessionImages = extractImagesFromSession(session);
1831
1894
  const extractedImages = [...sessionImages, ...userImages];
1832
1895
  images.push(...extractedImages);
1896
+ extractedImages.forEach((_, i) => refDescs.push(`\u3010\u4F60\u53D1\u9001\u7684\u3011\u7B2C ${i + 1} \u5F20`));
1833
1897
  const providedImages = extractedImages.length + aiProvidedImages;
1834
1898
  const remainingImages = Math.max(0, maxImages - providedImages);
1835
1899
  if (aiProvidedImages > 0 && remainingImages === 0) {
@@ -1846,6 +1910,7 @@ ${session.text(`image-prompt.messages.${hintKey}`, [selection.hint])}`;
1846
1910
  if (promptContent !== void 0) {
1847
1911
  const newImages = extractImagesFromMessage(promptContent);
1848
1912
  images.push(...newImages);
1913
+ newImages.forEach(() => refDescs.push("\u3010\u4F60\u53D1\u9001\u7684\u3011\u8865\u53D1\u7684\u56FE"));
1849
1914
  } else {
1850
1915
  break;
1851
1916
  }
@@ -1879,6 +1944,9 @@ Text in the image must be rendered exactly as shown in the attached text-referen
1879
1944
  }
1880
1945
  logInfo2(images);
1881
1946
  const promptEcho = config.showPrompt === false ? "" : buildPromptBlock(promptText, config.promptMaxLength || 4e3);
1947
+ const confirmMode = cmdConfig.confirmBeforeDraw;
1948
+ const confirmOn = confirmMode === "off" || confirmMode === false ? false : confirmMode === "on" || confirmMode === true ? true : config.confirmBeforeDraw !== false;
1949
+ const confirmTimeout = typeof config.confirmTimeout === "number" ? config.confirmTimeout : 60;
1882
1950
  try {
1883
1951
  if (config.mergeNotifications === false) {
1884
1952
  await reply(session, [session.text("image-prompt.messages.processing") + selectionNote + (optimizeFailed ? "\n" + session.text("image-prompt.messages.optimizefailed") : "")]);
@@ -1900,6 +1968,20 @@ Text in the image must be rendered exactly as shown in the attached text-referen
1900
1968
  await reply(session, [session.text("image-prompt.messages.invalidimage")]);
1901
1969
  return;
1902
1970
  }
1971
+ if (confirmOn) {
1972
+ const refList = refDescs.length ? refDescs.map((desc, i) => `${i + 1}. ${desc}`).join("\n") : `\uFF08\u56FE\u5E93\u91CC\u6CA1\u6709\u5339\u914D\u7684\u53C2\u8003\u56FE\uFF0C\u5C06\u6309\u63D0\u793A\u8BCD\u751F\u6210\uFF0C\u5171 ${files.length} \u5F20\u8F93\u5165\uFF09`;
1973
+ notify(session.text("image-prompt.messages.confirmask", [
1974
+ refList,
1975
+ confirmTimeout > 0 ? String(confirmTimeout) : "\u4E0D\u9650"
1976
+ ]));
1977
+ await flushNotice();
1978
+ const confirmed = await waitForConfirm(session, confirmTimeout);
1979
+ if (!confirmed) {
1980
+ await reply(session, [session.text("image-prompt.messages.confirmcancel")]);
1981
+ return;
1982
+ }
1983
+ notify(session.text("image-prompt.messages.confirmok"));
1984
+ }
1903
1985
  if (config.backgroundDrawing?.enabled) {
1904
1986
  const snapshot = {
1905
1987
  bot: session.bot,
@@ -1915,8 +1997,7 @@ Text in the image must be rendered exactly as shown in the attached text-referen
1915
1997
  }
1916
1998
  };
1917
1999
  const queued = enqueueDrawJob(snapshot, files);
1918
- notify(`${session.text("image-prompt.messages.bgstart", [files.length])}${queued > 0 ? session.text("image-prompt.messages.bgqueue", [queued]) : ""}${selectionNote}`);
1919
- notify(promptEcho);
2000
+ notify(`${session.text("image-prompt.messages.bgstart", [files.length])}${queued > 0 ? session.text("image-prompt.messages.bgqueue", [queued]) : ""}`);
1920
2001
  await flushNotice();
1921
2002
  return;
1922
2003
  }
@@ -1967,7 +2048,7 @@ Text in the image must be rendered exactly as shown in the attached text-referen
1967
2048
  }
1968
2049
  await saveToGallery(result, buildGalleryDescription(snapshot.promptText, ""), snapshot.commandName);
1969
2050
  await send([`${snapshot.texts.done}
1970
- `, import_koishi.h.image(result)]);
2051
+ `, await buildResultImagePart(result)]);
1971
2052
  } catch (error) {
1972
2053
  ctx.logger.error(`[${snapshot.commandName}] \u540E\u53F0\u7ED8\u56FE\u5931\u8D25:`, error);
1973
2054
  try {
@@ -2420,11 +2501,13 @@ ${lines.join("\n")}`]);
2420
2501
  }
2421
2502
  const scoreContent = (selector.scorePrompt || DEFAULT_SCORE_PROMPT).replace(/\{subject\}/g, analysis.subject || "\uFF08\u672A\u6307\u5B9A\uFF09").replace(/\{must\}/g, analysis.must.length ? analysis.must.join("\u3001") : "\uFF08\u65E0\uFF09").replace(/\{nice\}/g, analysis.nice.length ? analysis.nice.join("\u3001") : "\uFF08\u65E0\uFF09").replace(/\{avoid\}/g, analysis.avoid.length ? analysis.avoid.join("\u3001") : "\uFF08\u65E0\uFF09").replace(/\{candidates\}/g, buildIndexText(shortlist)).replace(/\{max\}/g, String(max));
2422
2503
  const visionOn = selector.vision === true;
2504
+ const inlined = visionOn ? await inlineCandidateImages(shortlist, selector.visionMaxImages || 6) : void 0;
2423
2505
  const scoreUserContent = buildSelectorContent(
2424
2506
  scoreContent,
2425
2507
  shortlist,
2426
2508
  visionOn,
2427
- selector.visionMaxImages || 6
2509
+ selector.visionMaxImages || 6,
2510
+ inlined
2428
2511
  );
2429
2512
  const scoreCall = await callSelectorModel(
2430
2513
  scoreUserContent,
@@ -2465,6 +2548,35 @@ ${lines.join("\n")}`]);
2465
2548
  hint: visionOn && selector.appendHint !== false ? hint : void 0
2466
2549
  };
2467
2550
  }
2551
+ async function inlineCandidateImages(candidates, max) {
2552
+ const selector = config.aiSelector || {};
2553
+ const cap = (selector.visionMaxImageBytes || 4) * 1024 * 1024;
2554
+ const out = new Array(candidates.length);
2555
+ const limit = max > 0 ? Math.min(max, candidates.length) : candidates.length;
2556
+ await Promise.all(candidates.map(async (candidate, index) => {
2557
+ if (index >= limit) return;
2558
+ const brief = (candidate.url || "").slice(0, 80);
2559
+ try {
2560
+ const file = await ctx.http.file(candidate.url);
2561
+ const data = file?.data;
2562
+ if (!data) throw new Error("\u6CA1\u6709\u62FF\u5230\u56FE\u7247\u6570\u636E");
2563
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
2564
+ if (!buffer.length) throw new Error("\u56FE\u7247\u662F\u7A7A\u7684");
2565
+ if (buffer.length > cap) {
2566
+ ctx.logger.warn(
2567
+ `\u5019\u9009\u56FE ${(buffer.length / 1048576).toFixed(1)}MB \u8D85\u8FC7\u4E0A\u9650 ${(cap / 1048576).toFixed(1)}MB\uFF0C\u8BC6\u56FE\u65F6\u53EA\u53D1\u6587\u5B57\u63CF\u8FF0\uFF1A${brief}`
2568
+ );
2569
+ return;
2570
+ }
2571
+ out[index] = `data:${file?.mime || "image/jpeg"};base64,${buffer.toString("base64")}`;
2572
+ } catch (error) {
2573
+ ctx.logger.warn(`\u5019\u9009\u56FE\u4E0B\u8F7D\u5931\u8D25\uFF0C\u8BC6\u56FE\u65F6\u53EA\u53D1\u6587\u5B57\u63CF\u8FF0\uFF1A${brief}\uFF08${error}\uFF09`);
2574
+ }
2575
+ }));
2576
+ const ok = out.filter(Boolean).length;
2577
+ logInfo2(`\u8BC6\u56FE\u5019\u9009\u56FE\u5DF2\u5185\u8054 ${ok}/${limit} \u5F20\uFF08\u5176\u4F59\u53EA\u53D1\u6587\u5B57\u63CF\u8FF0\uFF09`);
2578
+ return out;
2579
+ }
2468
2580
  function buildIndexText(candidates) {
2469
2581
  return candidates.map((c, i) => `[${i + 1}] \u6240\u5C5E\u7EC4\uFF1A${c.group} | \u63CF\u8FF0\uFF1A${c.description || "\uFF08\u65E0\u63CF\u8FF0\uFF09"}`).join("\n");
2470
2582
  }
@@ -2503,7 +2615,8 @@ ${lines.join("\n")}`]);
2503
2615
  const selector = config.aiSelector || {};
2504
2616
  const content = (selector.prompt || DEFAULT_SELECTOR_PROMPT).replace(/\{candidates\}/g, buildIndexText(candidates)).replace(/\{userInput\}/g, userInput || "\uFF08\u7528\u6237\u672A\u9644\u52A0\u8BF4\u660E\uFF09").replace(/\{max\}/g, String(max)).replace(/\{command\}/g, cmdConfig.name || "").replace(/\{prompt\}/g, promptText || "");
2505
2617
  const visionOn = selector.vision === true;
2506
- const userContent = buildSelectorContent(content, candidates, visionOn, selector.visionMaxImages || 6);
2618
+ const inlined = visionOn ? await inlineCandidateImages(candidates, selector.visionMaxImages || 6) : void 0;
2619
+ const userContent = buildSelectorContent(content, candidates, visionOn, selector.visionMaxImages || 6, inlined);
2507
2620
  const { raw, error } = await callSelectorModel(
2508
2621
  userContent,
2509
2622
  content,
@@ -2627,6 +2740,23 @@ ${lines.join("\n")}`]);
2627
2740
  }
2628
2741
  }
2629
2742
  }
2743
+ async function waitForConfirm(session, timeoutSec) {
2744
+ let answer;
2745
+ try {
2746
+ answer = await session.prompt((timeoutSec > 0 ? timeoutSec : 600) * 1e3);
2747
+ } catch (error) {
2748
+ ctx.logger.warn(`\u7B49\u5F85\u786E\u8BA4\u65F6\u51FA\u9519: ${error}`);
2749
+ return false;
2750
+ }
2751
+ if (answer === void 0 || answer === null) {
2752
+ logInfo2(`\u7B49\u5F85\u786E\u8BA4\u8D85\u65F6\uFF08${timeoutSec} \u79D2\u672A\u56DE\u590D\uFF09`);
2753
+ return false;
2754
+ }
2755
+ const text = extractTextFromMessage(answer);
2756
+ const ok = isConfirmInput(text);
2757
+ logInfo2(`\u5F00\u753B\u524D\u786E\u8BA4\uFF1A\u7528\u6237\u56DE\u590D ${JSON.stringify(text)} -> ${ok ? "\u5F00\u59CB\u753B" : "\u89C6\u4E3A\u53D6\u6D88"}`);
2758
+ return ok;
2759
+ }
2630
2760
  function extractTextFromMessage(content) {
2631
2761
  if (!content) return "";
2632
2762
  return import_koishi.h.select(content, "text").map((el) => el.attrs.content || "").join(" ").trim();
@@ -2807,6 +2937,7 @@ ${lines.join("\n")}`]);
2807
2937
  extractTextToRender,
2808
2938
  fitImageSize,
2809
2939
  inject,
2940
+ isConfirmInput,
2810
2941
  isPassiveReplyError,
2811
2942
  isRetryableStatus,
2812
2943
  isSkipReferenceInput,
@@ -2823,6 +2954,7 @@ ${lines.join("\n")}`]);
2823
2954
  previewJson,
2824
2955
  readImageSize,
2825
2956
  resolveTokenParam,
2957
+ sanitizeAskMessage,
2826
2958
  sanitizeCaption,
2827
2959
  selectByScore,
2828
2960
  stripCommandName,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-image-prompt",
3
3
  "description": "多功能 AI 图像处理插件,支持手办化、Coser化、Minecraft化等47种风格转换,并提供自定义提示词和多图融合功能",
4
- "version": "1.2.0",
4
+ "version": "1.3.0",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
package/readme.md CHANGED
@@ -7,8 +7,8 @@
7
7
  基于 OpenAI 兼容的 Chat Completions 接口,支持多种 AI 绘图模型,让你的图片瞬间变成精美手办。
8
8
 
9
9
  **特色能力**:参考图片分组 + AI 按需选图 · 两级检索选图 · 多模态识图还原 · 用户附加需求融合进提示词 ·
10
- 提示词回显(QQ 走 markdown 代码块)· 文字渲染参考图防中文崩字 · 生成结果自动入库形成闭环 ·
11
- 后台绘图 + 完成推送 · 参考图描述 AI 自动生成 · 429/限流自动退避重试。
10
+ 提示词回显(QQ 走 markdown 代码块)· **收到即回执 + 开画前确认** · 文字渲染参考图防中文崩字 ·
11
+ 生成结果自动入库形成闭环 · 后台绘图 + 完成推送 · 参考图描述 AI 自动生成 · 429/限流自动退避重试。
12
12
 
13
13
  ## ⚙️ 插件设置
14
14
 
@@ -96,6 +96,7 @@
96
96
  | 在处理提示中附带 AI 选图结果 | 回显选中了哪些参考图 | `true` |
97
97
  | 选图模型支持识别图片(多模态/VL) | 开启后把候选图片本身发给模型,而不只是发文字描述 | `false` |
98
98
  | 最多附带多少张候选图片 | 识图模式下限制图片数量,超出部分只发文字描述 | 6 |
99
+ | 识图单张图片体积上限(MB) | 插件自己下载候选图转 base64 再发;超过上限的只发文字描述 | 4 |
99
100
  | 带图片请求失败时退回纯文字再试一次 | 模型不支持识图时自动降级 | `true` |
100
101
  | 把模型看图后给出的关键视觉特征并入绘图提示词 | 让出图更还原参考图,仅识图模式生效 | `true` |
101
102
 
@@ -151,18 +152,51 @@
151
152
  ### 4. 交互流程
152
153
 
153
154
  1. 用户触发指令(可附带文字说明,自定义指令的文字会并入绘图提示词,同时也作为选图依据)。
155
+ **指令一触发就先回一条「收到,正在准备...」** —— 后面「优化提示词 + AI 选图」是两轮模型请求,
156
+ 加起来可能要几十秒,不发这条用户会以为机器人卡死了(可用「收到提示」关掉)。
154
157
  2. **打分制**:先做需求分析(不给候选图)→ 本地关键词召回 → 逐图显式打分 → 分数达标的才入选。
155
158
  旧版:把候选图片的「编号 + 所属组 + 描述」一次性交给模型,返回 `{"selected":[编号], ...}`。
156
159
  3. 若模型判定缺少合适的参考图(`needUserImage: true`),插件把 `askMessage` 发给用户,等待用户发送图片;
157
160
  用户发送的图片会被直接使用,若附带文字说明则再选一次。
158
161
  4. **一张都不达标时**(打分制):如实告诉用户「图库里没有合适的参考图」并给出打分原因,
159
162
  不会拿不相关的图硬凑;开了「缺少合适参考图时询问用户补充发送」的话会先请用户补一张参考图。
160
- 5. 选中的参考图 + 用户图片一起提交给绘图模型。
161
- 6. **AI 选中的参考图也算数**:指令要求的图片数量(maxImages)会把 AI 选中的图计入,
163
+ 5. **开画前确认**(默认开启):AI 挑完参考图后**不立刻画**,先把「这次会参考哪几张图」列出来,
164
+ 回复「确认」才真的开画(详见下一节)。
165
+ 6. 选中的参考图 + 用户图片一起提交给绘图模型。
166
+ 7. **AI 选中的参考图也算数**:指令要求的图片数量(maxImages)会把 AI 选中的图计入,
162
167
  已经够了就不再追问用户发图(例如「手办化」要 1 张图,AI 选中 1 张后直接开画)。
163
- 7. 选图**报错**时(走不通/超时,与「没有合适的图」是两回事):开启「回退」则使用候选池内全部图片;
168
+ 8. 选图**报错**时(走不通/超时,与「没有合适的图」是两回事):开启「回退」则使用候选池内全部图片;
164
169
  关闭(默认)则本次不带参考图,并在处理提示里说明选图未生效及原因。
165
170
 
171
+ ### ✅ 开画前确认(防止选错参考图白画一张)
172
+
173
+ | 配置项 | 说明 | 默认 |
174
+ |---|---|---|
175
+ | 开画前请用户确认 | 总开关;指令级可用「该指令是否在开画前请用户确认」覆盖 | 开 |
176
+ | 等待用户确认的时间(秒) | 超时按取消处理;0 = 不限时 | 60 |
177
+
178
+ 流程变成这样:
179
+
180
+ ```
181
+ 你发指令并附图
182
+ ↓
183
+ 「收到,正在准备...」 ← 立刻回,不让用户干等
184
+ ↓
185
+ 「正在挑选参考图片...」
186
+ ↓
187
+ 「准备开始绘图,本次会参考:
188
+ 1. 【图库】白发少女 双马尾 水手服 站立 日系厚涂
189
+ 2. 【你发送的】第 1 张
190
+ 回复「确认」开始画(60 秒内未确认则取消)」
191
+ ↓
192
+ 你回「确认」 → 开画
193
+ 你回别的 / 不回 → 「未收到确认,已取消本次绘图」
194
+ ```
195
+
196
+ - 认的确认词:`确认 / 确定 / 可以 / 好的 / 好 / 行 / 是 / 开始 / 开画 / 画吧 / 继续 / ok / yes / 1` 等**短回复**
197
+ (超过 12 个字或带别的句子都算取消,宁可让你重发指令,也不画错一张)。
198
+ - 只想直接出图、不想多这一步,就把「开画前请用户确认」关掉(或在某个指令上单独设成 `选好图直接开画`)。
199
+
166
200
  ### 📝 提示词回显
167
201
 
168
202
  处理时会把**最终优化后的提示词**一起发出来,方便你确认 AI 到底用了什么(含识图模式追加的「参考图关键特征」)。
@@ -368,7 +402,10 @@ rewrite 失败时自动退回 merge 的结果,不会因为优化失败而不
368
402
  勾选 **「选图模型支持识别图片(多模态/VL)」**,插件会按 `[1] 描述` → 图片 → `[2] 描述` → 图片 …
369
403
  的顺序把候选图片本身一起发过去,模型对着真实图片挑选,准确率明显更高。
370
404
 
371
- - 图片以 `image_url` 形式发送,**链接必须能被模型服务端访问**(公网直链,不能是需要登录或防盗链的图床)
405
+ - 图片**由插件自己下载后转成 base64 再发**(不是把链接丢给模型服务端去拉)。
406
+ 自己拉是有代价的:碰到私有 CDN / 防盗链的图床,服务端会返回
407
+ `Failed to download image from https://...` 并让**整条请求 400**,识图等于白开。
408
+ 下载失败或单张超过「识图单张图片体积上限」(默认 4MB)的图,就只发文字描述。
372
409
  - 「最多附带多少张候选图片」默认 6 张,超出部分仍发文字描述(避免请求体过大/超时)
373
410
  - 带图片请求失败(模型不支持、图片取不到)会自动退回纯文字再试一次,可用
374
411
  「带图片请求失败时退回纯文字再试一次」关闭
@@ -447,9 +484,13 @@ rewrite 失败时自动退回 merge 的结果,不会因为优化失败而不
447
484
  - **最好走一次 assets**:绘图接口给的虽然是公网链接,但手机端 QQ 经常拉不到(防盗链/域名)。
448
485
  开启后先经 assets 服务(`koishi-plugin-assets-qqbot-part-file` 等)转成平台可访问的地址;
449
486
  上传失败会自动用原链接,不会更差。
487
+ - **兜底会按字节发**:发送失败(QQ 报 `[40093007] 富媒体文件下载失败`)时,插件退回去
488
+ **自己把结果图下载下来、按字节交给适配器上传**,而不是把链接再丢给平台去拉。
489
+ 这条兜底对「平台服务器拉不到你的图床」这类问题基本是决定性的。
450
490
 
451
491
  | 配置项 | 说明 | 默认 |
452
492
  |---|---|---|
493
+ | 收到提示 | 指令一触发就先回一条「收到,正在准备...」 | 开 |
453
494
  | 合并消息 | 把过程提示合并成一条 | 开 |
454
495
  | 结果图用 markdown | 支持 markdown 的平台用 `![#Wpx #Hpx](url)` 发结果 | 开 |
455
496
  | 结果图经 assets 上传 | 外链手机端常拉不到,转成平台地址 | 开 |
@@ -458,8 +499,9 @@ rewrite 失败时自动退回 merge 的结果,不会因为优化失败而不
458
499
  | 手动宽 / 高 | 仅在关掉自动尺寸时生效,0 表示不限制 | 0 / 0 |
459
500
 
460
501
  细节:
461
- - 需要你**立刻回应**的提示(等你发图、等自定义提示词)会先把攒着的消息发出去再问
502
+ - 需要你**立刻回应**的提示(等你发图、等确认、等自定义提示词)会先把攒着的消息发出去再问
462
503
  - 合并后若超过 QQ markdown 单条长度上限,会自动把提示词代码块拆成第二条
504
+ - **开了后台绘图时,过程提示和提示词也只发一次**(以前会连同「已开始后台绘图」重复发一遍)
463
505
  - 关掉「合并消息」就回到逐条发送;不支持 markdown 的平台(频道、OneBot)自动用纯文本/普通图片消息
464
506
 
465
507
  ### 🩺 排查「AI 选图响应中没有文本内容」
package/src/index.ts CHANGED
@@ -38,15 +38,21 @@ export const usage = `
38
38
  若开启「缺少合适参考图时询问用户补充发送」,会请用户补发一张参考图(用户发的图会被直接使用)。
39
39
  想回到老行为就把「选图模式」改成「旧版」。
40
40
  5. 当用户明确说「不用参考 / 随意画 / 自由发挥」时,直接跳过选图,一次选图请求都不发。
41
- 6. 「启用 AI 智能选择参考图片」默认关闭,需手动开启;「AI 选图失败时回退为使用候选池内全部图片」默认关闭,
41
+ 6. 指令一触发就先回一条「收到,正在准备...」(「消息发送 → 收到提示」,默认开)。
42
+ 后面的优化提示词 + 选图是两轮模型请求,可能要几十秒,不发这条用户会以为机器人卡死。
43
+ 7. **开画前确认**(「消息发送 → 开画前请用户确认」,默认开):AI 挑完参考图先不画,
44
+ 把「本次会参考哪几张图」列出来,回复「确认」才开画(超时或回别的都算取消)。
45
+ 不想多这一步就关掉,或在单个指令上设成「选好图直接开画」。
46
+ 8. 「启用 AI 智能选择参考图片」默认关闭,需手动开启;「AI 选图失败时回退为使用候选池内全部图片」默认关闭,
42
47
  失败时本次不使用参考图片(开启则改用候选池内全部图片)。注意「回退」只对**请求失败**生效,
43
48
  对「打分不达标」不生效 —— 后者就该不带参考图。
44
- 7. 「旧版」模式下参考图较多时(超过 12 张)自动走两级检索:先让模型产出检索关键词、本地匹配召回,
49
+ 9. 「旧版」模式下参考图较多时(超过 12 张)自动走两级检索:先让模型产出检索关键词、本地匹配召回,
45
50
  再对召回结果精排;识图模式下也只发送召回的这几张图片,避免每次都把整个图库发给模型。
46
- 8. 描述可以用「生成描述」指令让模型看图自动生成并写回配置(需要支持图片输入的模型)。
47
- 若选图模型支持识别图片(多模态),可勾选「选图模型支持识别图片」,插件会把候选图片本身发给模型,
48
- 模型对着真实图片挑选;它给出的关键视觉特征还会并入绘图提示词,让出图更还原参考图。
49
- 9. 生成结果可自动入库(默认关闭),下次能被自己检索到并复用,形成闭环;
51
+ 10. 描述可以用「生成描述」指令让模型看图自动生成并写回配置(需要支持图片输入的模型)。
52
+ 若选图模型支持识别图片(多模态),可勾选「选图模型支持识别图片」,插件会把候选图片**自己下载后转 base64**
53
+ 再发给模型(直接丢链接的话,模型服务端拉不到就会整条请求 400),模型对着真实图片挑选;
54
+ 它给出的关键视觉特征还会并入绘图提示词,让出图更还原参考图。
55
+ 11. 生成结果可自动入库(默认关闭),下次能被自己检索到并复用,形成闭环;
50
56
  开启「后台绘图」后出图不再阻塞,先回「正在画」,画好主动推送。
51
57
 
52
58
  提示词模板可用占位符:{candidates} 候选图片列表、{userInput} 用户附加需求、{max} 最多选择数量、{command} 指令名、{prompt} 指令提示词
@@ -283,6 +289,37 @@ export function isSkipReferenceInput(userInput: string): boolean {
283
289
  return SKIP_REFERENCE_RE.test(text)
284
290
  }
285
291
 
292
+ /**
293
+ * 兜住「需求分析」模型爱乱加的废话。
294
+ *
295
+ * 实测模型会把 needTypes 写成
296
+ * 「能发一张你想用的自己形象的参考图吗?顺便说说 DeepSeek 画成鲸鱼可以吗?」——
297
+ * 后半句是凭空瞎聊,用户看了莫名其妙。这里只保留第一句、去掉换行和 markdown 修饰、
298
+ * 限制长度,把「询问补图」这件事还原成一句话。
299
+ */
300
+ export function sanitizeAskMessage(raw: string | null | undefined, max = 40): string {
301
+ let text = String(raw ?? '').replace(/\s+/g, ' ').trim()
302
+ if (!text) return ''
303
+ // 只留第一句:句末标点(中文/英文)之后的内容全部丢掉
304
+ const first = text.match(/^[^。!?!?;;]*[。!?!?;;]?/)
305
+ if (first) text = first[0].trim()
306
+ // 去掉 markdown 记号与包裹引号
307
+ text = text.replace(/^[-*>#\s]+/, '').replace(/^["'“”「」『』]+/, '').replace(/["'“”「」『』]+$/, '').trim()
308
+ if (text.length > max) text = text.slice(0, max) + '…'
309
+ return text
310
+ }
311
+
312
+ /** 确认绘图的回复:只认「就是这一句」的短回复,避免把正常聊天当确认 */
313
+ const CONFIRM_RE = /^(确认|确定|可以|好的|好|行|嗯|是|是的|开始|开画|画吧|继续|ok|okay|yes|y|1|\+1)$/i
314
+
315
+ export function isConfirmInput(raw: string | null | undefined): boolean {
316
+ const text = String(raw ?? '')
317
+ .replace(/[\s\u3000]+/g, '')
318
+ .replace(/[!!。.,,~~??::]+$/g, '')
319
+ if (!text || text.length > 12) return false
320
+ return CONFIRM_RE.test(text)
321
+ }
322
+
286
323
  /** 解析「需求抽取」的返回 */
287
324
  export function parseAnalysisResult(raw: string | null): RequirementAnalysis | null {
288
325
  const data = extractJsonObject(raw)
@@ -295,7 +332,7 @@ export function parseAnalysisResult(raw: string | null): RequirementAnalysis | n
295
332
  avoid: toStrList(data.avoid ?? data.exclude ?? data.negative ?? data.不要出现),
296
333
  text: Array.isArray(text) ? toStrList(text).join('\n') : String(text ?? '').trim(),
297
334
  skipReference: data.skipReference === true || data.skip_reference === true,
298
- needTypes: String(data.needTypes ?? data.need_types ?? data.需要的图 ?? '').trim(),
335
+ needTypes: sanitizeAskMessage(data.needTypes ?? data.need_types ?? data.需要的图 ?? ''),
299
336
  }
300
337
  }
301
338
 
@@ -895,12 +932,16 @@ export function withTokenParam(body: any, param: string): any {
895
932
  * @param candidates 候选图片
896
933
  * @param vision 是否附带图片
897
934
  * @param visionMaxImages 最多附带多少张图片
935
+ * @param dataUrls 已经自己下载好的 base64 data URL(按 candidates 下标)。**传了它之后,
936
+ * 没下载成功的图就不再发原始链接了** —— 上游拉不到那个链接会整条请求 400,
937
+ * 连文字描述一起废掉;只发描述反而还能用。
898
938
  */
899
939
  export function buildSelectorContent(
900
940
  text: string,
901
941
  candidates: CandidateImage[],
902
942
  vision: boolean,
903
- visionMaxImages: number
943
+ visionMaxImages: number,
944
+ dataUrls?: (string | undefined)[]
904
945
  ): string | any[] {
905
946
  if (!vision) return text
906
947
 
@@ -909,7 +950,8 @@ export function buildSelectorContent(
909
950
  candidates.forEach((candidate, index) => {
910
951
  parts.push({ type: 'text', text: `[${index + 1}] 所属组:${candidate.group} | 描述:${candidate.description || '(无描述)'}` })
911
952
  if (index < max) {
912
- parts.push({ type: 'image_url', image_url: { url: candidate.url } })
953
+ const url = dataUrls ? dataUrls[index] : candidate.url
954
+ if (url) parts.push({ type: 'image_url', image_url: { url } })
913
955
  }
914
956
  })
915
957
  const hintRule = visionMaxImages && candidates.length > 1
@@ -1026,6 +1068,8 @@ interface CommandConfig {
1026
1068
  aiSelect?: boolean
1027
1069
  aiMaxSelect?: number
1028
1070
  aiAskUser?: boolean
1071
+ /** 该指令是否在开画前请用户确认:'follow' 跟随全局 / 'on' 总是确认 / 'off' 从不确认 */
1072
+ confirmBeforeDraw?: 'follow' | 'on' | 'off' | boolean
1029
1073
  }[]
1030
1074
  }
1031
1075
  defaultWaitTimeout: number
@@ -1047,6 +1091,12 @@ interface CommandConfig {
1047
1091
  textRender?: TextRenderConfig
1048
1092
  /** 把处理过程中的多条提示合并成一条消息发出(省被动消息额度) */
1049
1093
  mergeNotifications?: boolean
1094
+ /** 指令一触发就立刻回一条「收到」,避免用户以为卡住 */
1095
+ ackOnStart?: boolean
1096
+ /** AI 选好参考图后先请用户确认再开画 */
1097
+ confirmBeforeDraw?: boolean
1098
+ /** 等待用户确认的时间(秒) */
1099
+ confirmTimeout?: number
1050
1100
  /** 结果图用 markdown 的 ![](url) 单独发一条(支持的平台) */
1051
1101
  markdownImage?: boolean
1052
1102
  /** 结果图先经 assets 服务上传再发(外链在手机端 QQ 可能拉不到) */
@@ -1150,6 +1200,8 @@ interface AISelectorConfig {
1150
1200
  notify: boolean
1151
1201
  vision: boolean
1152
1202
  visionMaxImages: number
1203
+ /** 识图时单张候选图的体积上限(MB),超过就只发文字描述 */
1204
+ visionMaxImageBytes?: number
1153
1205
  visionFallback: boolean
1154
1206
  appendHint: boolean
1155
1207
  twoStage: boolean
@@ -1299,6 +1351,7 @@ const DEFAULT_ANALYZE_PROMPT = `你是绘图参考图的「需求分析助手」
1299
1351
  6. skipReference:用户明确表示「不用参考 / 随意画 / 自由发挥 / 你看着办」时为 true。
1300
1352
  7. needTypes:图库里**没有**合适参考图时,插件要请用户补发一张。这里写一句**直接对用户说**的话
1301
1353
  (中文,40 字以内,语气自然,不要用「请提供图片」这种套话),例如「能发一张白发水手服的立绘吗?」。
1354
+ **只写这一句**:不要追问第二件事、不要闲聊、不要提到模型名字(如 DeepSeek)、不要写「顺便说说」之类的话。
1302
1355
  图库里不缺图就留空字符串。
1303
1356
 
1304
1357
  只输出一个 JSON 对象,不要解释、不要 Markdown 代码块:
@@ -1784,7 +1837,12 @@ export const Config: Schema = Schema.intersect([
1784
1837
  referenceGroups: Schema.array(Schema.string()).description('引用的参考图片组名称(AI 将从这些组中挑选图片,留空则由「AI 选图设置」决定是否使用全部组)').default([]),
1785
1838
  aiSelect: Schema.boolean().default(true).description('启用 AI 智能选择参考图片'),
1786
1839
  aiMaxSelect: Schema.number().default(0).min(0).max(10).step(1).description('AI 最多为该指令选择的参考图片数量(0 = 跟随「AI 选图设置」里的全局数量)'),
1787
- aiAskUser: Schema.boolean().default(true).description('缺少合适参考图时询问用户补充发送')
1840
+ aiAskUser: Schema.boolean().default(true).description('缺少合适参考图时询问用户补充发送'),
1841
+ confirmBeforeDraw: Schema.union([
1842
+ Schema.const('follow').description('跟随全局设置'),
1843
+ Schema.const('on').description('开画前请用户确认'),
1844
+ Schema.const('off').description('选好图直接开画'),
1845
+ ]).default('follow').description('该指令是否在开画前请用户确认'),
1788
1846
  })).description('指令配置').default(defaultCommands),
1789
1847
  }).collapse().description('指令配置项太长啦,这样折叠起来更方便哦~'),
1790
1848
 
@@ -1866,6 +1924,7 @@ export const Config: Schema = Schema.intersect([
1866
1924
  notify: Schema.boolean().default(true).description('在处理提示中附带 AI 选图结果'),
1867
1925
  vision: Schema.boolean().default(false).description('选图模型支持识别图片(多模态/VL)。开启后会把候选图片本身一起发给模型,而不只是发文字描述,选得更准(需要图片链接能被模型访问)'),
1868
1926
  visionMaxImages: Schema.number().default(6).min(1).max(20).step(1).description('开启识别图片时,最多附带多少张候选图片(超出部分只发文字描述,避免请求过大)'),
1927
+ visionMaxImageBytes: Schema.number().default(4).min(0).max(32).step(1).description('识图时单张候选图的体积上限(MB)。插件会先把图片自己下载下来转成 base64 再发给选图模型(上游拉不到你的图片链接时会整条请求 400),超过上限的只发文字描述'),
1869
1928
  visionFallback: Schema.boolean().default(true).description('带图片请求失败时,自动退回纯文字再试一次'),
1870
1929
  appendHint: Schema.boolean().default(true).description('把模型看图后给出的关键视觉特征并入绘图提示词(仅在开启识别图片时生效,让出图更还原参考图)'),
1871
1930
  twoStage: Schema.boolean().default(true).description('【仅旧版模式生效】两级检索选图:先让模型产出检索关键词并在本地召回,再对召回结果精排。打分制已内置召回,无需此项'),
@@ -1934,6 +1993,9 @@ export const Config: Schema = Schema.intersect([
1934
1993
 
1935
1994
  Schema.object({
1936
1995
  mergeNotifications: Schema.boolean().default(true).description('把「选图结果 / 正在处理 / 优化后提示词」合并成**一条**消息发出(QQ 被动回复有次数上限,消息越少越稳)'),
1996
+ ackOnStart: Schema.boolean().default(true).description('指令一触发就立刻回一条「收到,正在准备...」。后面的「优化提示词 + AI 选图」是两轮模型请求、可能要几十秒,不发这条用户会以为机器人卡住了'),
1997
+ confirmBeforeDraw: Schema.boolean().default(true).description('AI 选好参考图后**先请用户确认再开始画**(把「本次参考这几张图」列出来,回复「确认」才画)。可防止选错参考图白画一张'),
1998
+ confirmTimeout: Schema.number().default(60).min(5).max(300).step(5).description('等待用户确认的时间(秒),超时按取消处理;0 = 不限时'),
1937
1999
  markdownImage: Schema.boolean().default(true).description('生成结果用 markdown 的 ![](图片链接) 单独发一条;不支持 markdown 的平台自动退回普通图片消息'),
1938
2000
  imageViaAssets: Schema.boolean().default(true).description('结果图先经 assets 服务上传再发。外链在手机端 QQ 常常拉不到,装上 koishi-plugin-assets-qqbot-part-file 之类的 assets 插件后由它转成平台可访问的地址;上传失败会自动用原链接'),
1939
2001
  autoImageSize: Schema.boolean().default(true).description('自动按**图片真实比例**生成 markdown 尺寸(填死宽高会把非方形图拉伸变形)'),
@@ -2080,7 +2142,33 @@ export function apply(ctx: Context, config: CommandConfig) {
2080
2142
  ctx.logger.warn(`markdown 图片发送失败,退回普通图片消息: ${error}`)
2081
2143
  }
2082
2144
  }
2083
- await reply(session, [h.image(finalUrl)])
2145
+ // 退回的普通图片消息也走「下载字节再发」:把外链丢给平台,
2146
+ // 平台自己拉不到时会报 [40093007] 富媒体文件下载失败。
2147
+ await reply(session, [await buildResultImagePart(finalUrl)])
2148
+ }
2149
+
2150
+ /**
2151
+ * 结果图优先用「自己下载好的字节」发。
2152
+ *
2153
+ * 给平台一个外链,是让平台服务器自己去下载它 —— 手机端 QQ 拉不到绘图接口给的链接时
2154
+ * 就报 `[40093007] 富媒体文件下载失败`。把字节交给适配器上传就没这个问题。
2155
+ * 下载失败再退回发原始链接(至少不比现在更差)。
2156
+ */
2157
+ async function buildResultImagePart(url: string): Promise<any> {
2158
+ try {
2159
+ const file = await ctx.http.file(url)
2160
+ const data = file?.data
2161
+ if (data) {
2162
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data)
2163
+ if (buffer.length) {
2164
+ logInfo(`结果图已下载(${(buffer.length / 1024).toFixed(0)}kB),按字节发送`)
2165
+ return h.image(buffer, file?.mime || 'image/png')
2166
+ }
2167
+ }
2168
+ } catch (error) {
2169
+ ctx.logger.warn(`结果图下载失败,改发原始链接: ${error}`)
2170
+ }
2171
+ return h.image(url)
2084
2172
  }
2085
2173
 
2086
2174
  /** 后台任务用 bot 直接发消息(没有 session):引用失败时同样降级 */
@@ -2115,6 +2203,10 @@ export function apply(ctx: Context, config: CommandConfig) {
2115
2203
  customprompt: '请在{0}秒内输入自定义提示词...',
2116
2204
  invalidimage: '未检测到有效的图片,请重新发送带图片的消息',
2117
2205
  processing: '正在处理图片,请稍候...',
2206
+ ack: '收到,正在准备...',
2207
+ confirmask: '准备开始绘图,本次会参考:\n{0}\n\n回复「确认」开始画({1} 秒内未确认则取消)',
2208
+ confirmcancel: '未收到确认,已取消本次绘图',
2209
+ confirmok: '已确认,开始绘图...',
2118
2210
  failed: '图片生成失败,请稍后重试',
2119
2211
  error: '处理过程中发生错误,请稍后重试',
2120
2212
  needprompt: '请提供自定义提示词',
@@ -2190,6 +2282,17 @@ export function apply(ctx: Context, config: CommandConfig) {
2190
2282
  }
2191
2283
  logInfo(`用户附加需求: ${userInputText || '(无)'}`)
2192
2284
 
2285
+ // ============ 立刻回一条「收到」 ============
2286
+ // 后面「优化提示词 + AI 选图」是两轮模型请求,加起来可能要几十秒,
2287
+ // 期间一句话都不发,用户会以为机器人在卡死(连指令有没有被识别都不知道)。
2288
+ if (config.ackOnStart !== false) {
2289
+ try {
2290
+ await reply(session, [session.text('image-prompt.messages.ack')])
2291
+ } catch (error) {
2292
+ ctx.logger.warn(`发送「收到」提示失败: ${error}`)
2293
+ }
2294
+ }
2295
+
2193
2296
  // ============ 合并消息(省 QQ 被动消息额度) ============
2194
2297
  // 「正在选图 / 处理中 / 优化后的提示词」先攒着,最后合成一条发出去。
2195
2298
  // 只有需要用户立刻回应(等你发图)时才提前 flush。
@@ -2275,6 +2378,8 @@ export function apply(ctx: Context, config: CommandConfig) {
2275
2378
  let selectionNote = ''
2276
2379
  let aiProvidedImages = 0 // AI 选中的参考图数量(计入「还需用户提供」的抵扣)
2277
2380
  let aiRenderText = '' // 选图模型认为画面上要出现的文字(台词/标题/字幕)
2381
+ // 本次要参考的图片清单(给「开画前确认」用)
2382
+ const refDescs: string[] = []
2278
2383
  if (aiSelectOn) {
2279
2384
  const candidates = collectCandidates(cmdConfig, defaultImageUrls)
2280
2385
  const declaredGroups = (cmdConfig.referenceGroups || []).filter(Boolean)
@@ -2323,6 +2428,7 @@ export function apply(ctx: Context, config: CommandConfig) {
2323
2428
  if (selection.ok && selection.picked.length > 0) {
2324
2429
  images.push(...selection.picked.map(c => c.url))
2325
2430
  aiProvidedImages += selection.picked.length
2431
+ for (const c of selection.picked) refDescs.push(`【图库】${c.description || c.url}`)
2326
2432
  logInfo(`AI 选中参考图片: ${selection.picked.map(c => c.url).join(' , ')} 理由: ${selection.reason}`)
2327
2433
 
2328
2434
  // 模型看图后给出的特征/画面编排并入绘图提示词(仅识别图片模式)
@@ -2397,6 +2503,7 @@ export function apply(ctx: Context, config: CommandConfig) {
2397
2503
  const sessionImages = extractImagesFromSession(session)
2398
2504
  const extractedImages = [...sessionImages, ...userImages]
2399
2505
  images.push(...extractedImages)
2506
+ extractedImages.forEach((_, i) => refDescs.push(`【你发送的】第 ${i + 1} 张`))
2400
2507
 
2401
2508
  // 计算还需要用户提供的图片数量(AI 选中的参考图也算数,已有图就不再追问)
2402
2509
  const providedImages = extractedImages.length + aiProvidedImages
@@ -2418,6 +2525,7 @@ export function apply(ctx: Context, config: CommandConfig) {
2418
2525
  if (promptContent !== undefined) {
2419
2526
  const newImages = extractImagesFromMessage(promptContent)
2420
2527
  images.push(...newImages)
2528
+ newImages.forEach(() => refDescs.push('【你发送的】补发的图'))
2421
2529
  } else {
2422
2530
  break
2423
2531
  }
@@ -2464,6 +2572,15 @@ export function apply(ctx: Context, config: CommandConfig) {
2464
2572
  ? ''
2465
2573
  : buildPromptBlock(promptText, config.promptMaxLength || 4000)
2466
2574
 
2575
+ // 开画前是否要用户确认:指令级 'on'/'off' 覆盖全局,'follow'/未填跟随全局
2576
+ const confirmMode = cmdConfig.confirmBeforeDraw
2577
+ const confirmOn = confirmMode === 'off' || confirmMode === false
2578
+ ? false
2579
+ : confirmMode === 'on' || confirmMode === true
2580
+ ? true
2581
+ : config.confirmBeforeDraw !== false
2582
+ const confirmTimeout = typeof config.confirmTimeout === 'number' ? config.confirmTimeout : 60
2583
+
2467
2584
  try {
2468
2585
  if (config.mergeNotifications === false) {
2469
2586
  await reply(session, [session.text('image-prompt.messages.processing') + selectionNote
@@ -2494,6 +2611,27 @@ export function apply(ctx: Context, config: CommandConfig) {
2494
2611
  return
2495
2612
  }
2496
2613
 
2614
+ // ============ 开画前确认 ============
2615
+ // 到这一步 AI 已经把参考图挑完了。直接把画发出去太亏 —— 万一挑错,
2616
+ // 用户只能白等一张图。所以先把「这次会参考哪几张」摆出来,回复「确认」才开画。
2617
+ if (confirmOn) {
2618
+ const refList = refDescs.length
2619
+ ? refDescs.map((desc, i) => `${i + 1}. ${desc}`).join('\n')
2620
+ : `(图库里没有匹配的参考图,将按提示词生成,共 ${files.length} 张输入)`
2621
+ notify(session.text('image-prompt.messages.confirmask', [
2622
+ refList,
2623
+ confirmTimeout > 0 ? String(confirmTimeout) : '不限',
2624
+ ]))
2625
+ // 必须先把清单发出去:用户得知道自己在确认什么
2626
+ await flushNotice()
2627
+ const confirmed = await waitForConfirm(session, confirmTimeout)
2628
+ if (!confirmed) {
2629
+ await reply(session, [session.text('image-prompt.messages.confirmcancel')])
2630
+ return
2631
+ }
2632
+ notify(session.text('image-prompt.messages.confirmok'))
2633
+ }
2634
+
2497
2635
  // 后台绘图:先把消息还给用户,出图在后台跑,完成后主动推送
2498
2636
  if (config.backgroundDrawing?.enabled) {
2499
2637
  const snapshot = {
@@ -2510,8 +2648,9 @@ export function apply(ctx: Context, config: CommandConfig) {
2510
2648
  }
2511
2649
  }
2512
2650
  const queued = enqueueDrawJob(snapshot, files)
2513
- notify(`${session.text('image-prompt.messages.bgstart', [files.length])}${queued > 0 ? session.text('image-prompt.messages.bgqueue', [queued]) : ''}${selectionNote}`)
2514
- notify(promptEcho)
2651
+ // 只补「已开始后台绘图」这一行。**不要**再把 selectionNote / promptEcho
2652
+ // 加一遍 —— 上面已经进过 noticeLines 队列了,再加就会连同提示词一起发两遍。
2653
+ notify(`${session.text('image-prompt.messages.bgstart', [files.length])}${queued > 0 ? session.text('image-prompt.messages.bgqueue', [queued]) : ''}`)
2515
2654
  await flushNotice()
2516
2655
  return
2517
2656
  }
@@ -2571,7 +2710,7 @@ export function apply(ctx: Context, config: CommandConfig) {
2571
2710
  return
2572
2711
  }
2573
2712
  await saveToGallery(result, buildGalleryDescription(snapshot.promptText, ''), snapshot.commandName)
2574
- await send([`${snapshot.texts.done}\n`, h.image(result)])
2713
+ await send([`${snapshot.texts.done}\n`, await buildResultImagePart(result)])
2575
2714
  } catch (error) {
2576
2715
  ctx.logger.error(`[${snapshot.commandName}] 后台绘图失败:`, error)
2577
2716
  try {
@@ -3190,8 +3329,13 @@ export function apply(ctx: Context, config: CommandConfig) {
3190
3329
  .replace(/\{max\}/g, String(max))
3191
3330
 
3192
3331
  const visionOn = selector.vision === true
3332
+ // 识图模式:候选图先自己下载成 base64。直接把原 URL 交给上游,
3333
+ // 上游拉不到(私有 CDN / 防盗链)会整条请求 400,连文字描述一起废掉。
3334
+ const inlined = visionOn
3335
+ ? await inlineCandidateImages(shortlist, selector.visionMaxImages || 6)
3336
+ : undefined
3193
3337
  const scoreUserContent = buildSelectorContent(
3194
- scoreContent, shortlist, visionOn, selector.visionMaxImages || 6
3338
+ scoreContent, shortlist, visionOn, selector.visionMaxImages || 6, inlined
3195
3339
  )
3196
3340
  const scoreCall = await callSelectorModel(
3197
3341
  scoreUserContent, scoreContent, visionOn, selector.selectMaxTokens || 8000,
@@ -3236,6 +3380,53 @@ export function apply(ctx: Context, config: CommandConfig) {
3236
3380
  }
3237
3381
  }
3238
3382
 
3383
+ /**
3384
+ * 识图模式用:把候选图自己下载成 base64 data URL。
3385
+ *
3386
+ * 不能直接把图片链接丢给上游模型 —— 上游是它自己去下载的,
3387
+ * 碰到私有 CDN / 防盗链(实测 pro.filesystem.site 就是)就会返回
3388
+ * 「Failed to download image from ...」并让**整条请求 400**,
3389
+ * 于是只能退回纯文字选图,识图功能白开。
3390
+ *
3391
+ * 下载失败或图片过大的,对应位置留 undefined → buildSelectorContent 只发文字描述。
3392
+ * @returns 与 candidates 同下标的 data URL 数组
3393
+ */
3394
+ async function inlineCandidateImages(
3395
+ candidates: CandidateImage[],
3396
+ max: number
3397
+ ): Promise<(string | undefined)[]> {
3398
+ const selector: AISelectorConfig = config.aiSelector || ({} as AISelectorConfig)
3399
+ const cap = (selector.visionMaxImageBytes || 4) * 1024 * 1024
3400
+ const out: (string | undefined)[] = new Array(candidates.length)
3401
+ const limit = max > 0 ? Math.min(max, candidates.length) : candidates.length
3402
+
3403
+ await Promise.all(candidates.map(async (candidate, index) => {
3404
+ if (index >= limit) return
3405
+ const brief = (candidate.url || '').slice(0, 80)
3406
+ try {
3407
+ const file = await ctx.http.file(candidate.url)
3408
+ const data = file?.data
3409
+ if (!data) throw new Error('没有拿到图片数据')
3410
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data)
3411
+ if (!buffer.length) throw new Error('图片是空的')
3412
+ if (buffer.length > cap) {
3413
+ ctx.logger.warn(
3414
+ `候选图 ${(buffer.length / 1048576).toFixed(1)}MB 超过上限 `
3415
+ + `${(cap / 1048576).toFixed(1)}MB,识图时只发文字描述:${brief}`
3416
+ )
3417
+ return
3418
+ }
3419
+ out[index] = `data:${file?.mime || 'image/jpeg'};base64,${buffer.toString('base64')}`
3420
+ } catch (error) {
3421
+ ctx.logger.warn(`候选图下载失败,识图时只发文字描述:${brief}(${error})`)
3422
+ }
3423
+ }))
3424
+
3425
+ const ok = out.filter(Boolean).length
3426
+ logInfo(`识图候选图已内联 ${ok}/${limit} 张(其余只发文字描述)`)
3427
+ return out
3428
+ }
3429
+
3239
3430
  /** 候选图片索引文本 */
3240
3431
  function buildIndexText(candidates: CandidateImage[]): string {
3241
3432
  return candidates
@@ -3310,8 +3501,12 @@ export function apply(ctx: Context, config: CommandConfig) {
3310
3501
  .replace(/\{prompt\}/g, promptText || '')
3311
3502
 
3312
3503
  // 支持识别图片时,把候选图片本身一起发给模型(编号文字 + 图片交替)
3504
+ // 图片先自己下载转 base64:上游拉不到原链接会整条请求 400
3313
3505
  const visionOn = selector.vision === true
3314
- const userContent = buildSelectorContent(content, candidates, visionOn, selector.visionMaxImages || 6)
3506
+ const inlined = visionOn
3507
+ ? await inlineCandidateImages(candidates, selector.visionMaxImages || 6)
3508
+ : undefined
3509
+ const userContent = buildSelectorContent(content, candidates, visionOn, selector.visionMaxImages || 6, inlined)
3315
3510
 
3316
3511
  const { raw, error } = await callSelectorModel(
3317
3512
  userContent, content, visionOn, selector.selectMaxTokens || 8000
@@ -3483,6 +3678,29 @@ export function apply(ctx: Context, config: CommandConfig) {
3483
3678
  }
3484
3679
  }
3485
3680
 
3681
+ /**
3682
+ * 等用户确认开画。
3683
+ * 复用「等你发图」那套 session.prompt:拿到用户在这个频道的下一条消息再判断。
3684
+ * 超时、或者回了别的话,都算取消(宁可让用户重发指令,也不要画错一张)。
3685
+ */
3686
+ async function waitForConfirm(session: Session, timeoutSec: number): Promise<boolean> {
3687
+ let answer: string | undefined
3688
+ try {
3689
+ answer = await session.prompt((timeoutSec > 0 ? timeoutSec : 600) * 1000)
3690
+ } catch (error) {
3691
+ ctx.logger.warn(`等待确认时出错: ${error}`)
3692
+ return false
3693
+ }
3694
+ if (answer === undefined || answer === null) {
3695
+ logInfo(`等待确认超时(${timeoutSec} 秒未回复)`)
3696
+ return false
3697
+ }
3698
+ const text = extractTextFromMessage(answer)
3699
+ const ok = isConfirmInput(text)
3700
+ logInfo(`开画前确认:用户回复 ${JSON.stringify(text)} -> ${ok ? '开始画' : '视为取消'}`)
3701
+ return ok
3702
+ }
3703
+
3486
3704
  /** 从消息中提取纯文本 */
3487
3705
  function extractTextFromMessage(content: string): string {
3488
3706
  if (!content) return ''