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 +25 -2
- package/lib/index.js +148 -16
- package/package.json +1 -1
- package/readme.md +49 -7
- package/src/index.ts +234 -16
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\
|
|
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 的  单独发一条(支持的平台) */
|
|
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. \
|
|
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
|
-
|
|
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
|
-
|
|
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\
|
|
116
|
-
\u6A21\u578B\
|
|
117
|
-
|
|
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:
|
|
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
|
-
|
|
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  \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, [
|
|
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]) : ""}
|
|
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
|
-
`,
|
|
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
|
|
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
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
|
-
|
|
163
|
+
5. **开画前确认**(默认开启):AI 挑完参考图后**不立刻画**,先把「这次会参考哪几张图」列出来,
|
|
164
|
+
回复「确认」才真的开画(详见下一节)。
|
|
165
|
+
6. 选中的参考图 + 用户图片一起提交给绘图模型。
|
|
166
|
+
7. **AI 选中的参考图也算数**:指令要求的图片数量(maxImages)会把 AI 选中的图计入,
|
|
162
167
|
已经够了就不再追问用户发图(例如「手办化」要 1 张图,AI 选中 1 张后直接开画)。
|
|
163
|
-
|
|
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
|
-
-
|
|
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 的平台用 `` 发结果 | 开 |
|
|
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.
|
|
41
|
+
6. 指令一触发就先回一条「收到,正在准备...」(「消息发送 → 收到提示」,默认开)。
|
|
42
|
+
后面的优化提示词 + 选图是两轮模型请求,可能要几十秒,不发这条用户会以为机器人卡死。
|
|
43
|
+
7. **开画前确认**(「消息发送 → 开画前请用户确认」,默认开):AI 挑完参考图先不画,
|
|
44
|
+
把「本次会参考哪几张图」列出来,回复「确认」才开画(超时或回别的都算取消)。
|
|
45
|
+
不想多这一步就关掉,或在单个指令上设成「选好图直接开画」。
|
|
46
|
+
8. 「启用 AI 智能选择参考图片」默认关闭,需手动开启;「AI 选图失败时回退为使用候选池内全部图片」默认关闭,
|
|
42
47
|
失败时本次不使用参考图片(开启则改用候选池内全部图片)。注意「回退」只对**请求失败**生效,
|
|
43
48
|
对「打分不达标」不生效 —— 后者就该不带参考图。
|
|
44
|
-
|
|
49
|
+
9. 「旧版」模式下参考图较多时(超过 12 张)自动走两级检索:先让模型产出检索关键词、本地匹配召回,
|
|
45
50
|
再对召回结果精排;识图模式下也只发送召回的这几张图片,避免每次都把整个图库发给模型。
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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:
|
|
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
|
-
|
|
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 的  单独发一条(支持的平台) */
|
|
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
|
-
|
|
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
|
-
|
|
2514
|
-
|
|
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`,
|
|
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
|
|
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 ''
|