koishi-plugin-image-prompt 2.0.4 → 2.1.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
@@ -76,6 +76,30 @@ export declare function renderAgentInstructions(template: string, vars: Record<s
76
76
  * 这里压成一行、去掉 markdown 记号和包裹引号、限长,让它永远是「对用户说的一句话」。
77
77
  */
78
78
  export declare function cleanAskMessage(raw: any, max?: number): string;
79
+ /**
80
+ * 从用户的回答里判断他要不要润色提示词。
81
+ * 默认 false —— 用户的原话原封不动交给绘图模型才是对的默认值。
82
+ */
83
+ export declare function parsePolishIntent(text: string): boolean;
84
+ /** 从消息内容里解析 @ 到的人。type="all"(@全体成员)和 @机器人自己 会被跳过 */
85
+ export declare function extractAtIds(content: string, selfId?: string): {
86
+ id: string;
87
+ name: string;
88
+ }[];
89
+ /**
90
+ * 拼头像地址。优先级:自定义模板 > 适配器直接给的 > 平台内置规则。
91
+ * 拼不出来返回空字符串(调用方再决定是问适配器还是放弃)。
92
+ */
93
+ export declare function buildAvatarUrl(input: {
94
+ userId: string;
95
+ platform?: string;
96
+ selfId?: string;
97
+ appId?: string;
98
+ /** 发送者自己的头像(适配器已经在消息里带过来了) */
99
+ authorAvatar?: string;
100
+ template?: string;
101
+ size?: number;
102
+ }): string;
79
103
  /**
80
104
  * 图库容量裁剪:超出容量时淘汰最旧的(按 time 升序),只保留每个组最近 capacity 条
81
105
  */
@@ -206,6 +230,7 @@ interface CommandConfig {
206
230
  referenceGroups?: ReferenceGroup[];
207
231
  aiSelector?: AISelectorConfig;
208
232
  agent?: AgentConfig;
233
+ avatar?: AvatarConfig;
209
234
  showPrompt?: boolean;
210
235
  promptMaxLength?: number;
211
236
  appendUserInput?: boolean;
@@ -317,6 +342,23 @@ interface AgentConfig {
317
342
  historyTurns: number;
318
343
  /** 把每一轮的工具调用打到日志里,方便排查 */
319
344
  debugLog: boolean;
345
+ /** 用户没给任何参考图时,开画前先问一句「要不要参考图」 */
346
+ askIfNoReference: boolean;
347
+ /** 开画前先问一句「要不要润色提示词」(默认不润色,原话直出) */
348
+ askBeforePolish: boolean;
349
+ }
350
+ /** 头像参考图配置 */
351
+ interface AvatarConfig {
352
+ /** 总开关:能不能拿用户头像当参考图 */
353
+ enabled: boolean;
354
+ /** 用户 @ 了谁,就自动把那个人的头像作为参考图(@ 本身就是明确指定) */
355
+ autoAt: boolean;
356
+ /** 用户没发图也没 @ 人时,自动拿他自己的头像当参考图 */
357
+ autoSelf: boolean;
358
+ /** 头像尺寸(边长像素) */
359
+ size: number;
360
+ /** 自定义头像地址模板,留空用内置规则。占位符:{userId} {platform} {selfId} {appId} {size} */
361
+ urlTemplate: string;
320
362
  }
321
363
  /** agent 参考图登记表里的一条 */
322
364
  export interface RefEntry {
@@ -324,7 +366,7 @@ export interface RefEntry {
324
366
  url: string;
325
367
  description: string;
326
368
  group: string;
327
- source: 'gallery' | 'user' | 'cmd';
369
+ source: 'gallery' | 'user' | 'cmd' | 'avatar';
328
370
  }
329
371
  /** 交给 AI 挑选的候选图片 */
330
372
  interface CandidateImage {
package/lib/index.js CHANGED
@@ -33,12 +33,14 @@ __export(src_exports, {
33
33
  Config: () => Config,
34
34
  apply: () => apply,
35
35
  buildAgentTools: () => buildAgentTools,
36
+ buildAvatarUrl: () => buildAvatarUrl,
36
37
  buildMarkdownImage: () => buildMarkdownImage,
37
38
  buildPromptBlock: () => buildPromptBlock,
38
39
  buildPromptEcho: () => buildPromptEcho,
39
40
  cleanAskMessage: () => cleanAskMessage,
40
41
  computeRetryDelay: () => computeRetryDelay,
41
42
  describeSelectorError: () => describeSelectorError,
43
+ extractAtIds: () => extractAtIds,
42
44
  extractJsonObject: () => extractJsonObject,
43
45
  extractResponseText: () => extractResponseText,
44
46
  extractServerMessage: () => extractServerMessage,
@@ -50,6 +52,7 @@ __export(src_exports, {
50
52
  mergePrompt: () => mergePrompt,
51
53
  name: () => name,
52
54
  parseJsonToolCall: () => parseJsonToolCall,
55
+ parsePolishIntent: () => parsePolishIntent,
53
56
  parseRetryAfterHeader: () => parseRetryAfterHeader,
54
57
  pickAgentMessage: () => pickAgentMessage,
55
58
  previewJson: () => previewJson,
@@ -226,7 +229,7 @@ function resolveReferences(refs, registry) {
226
229
  }
227
230
  return { ids, unknown };
228
231
  }
229
- var AGENT_TOOL_NAMES = ["gallery_search", "ask_user", "draw"];
232
+ var AGENT_TOOL_NAMES = ["gallery_search", "ask_user", "get_avatar", "draw"];
230
233
  function buildAgentTools() {
231
234
  return [
232
235
  {
@@ -258,6 +261,20 @@ function buildAgentTools() {
258
261
  }
259
262
  }
260
263
  },
264
+ {
265
+ type: "function",
266
+ function: {
267
+ name: "get_avatar",
268
+ description: "\u53D6\u67D0\u4E2A\u4EBA\u7684\u5934\u50CF\u5F53\u53C2\u8003\u56FE\uFF0C\u8FD4\u56DE\u5B83\u7684\u7F16\u53F7\u3002userId \u7559\u7A7A = \u53D6\u53D1\u6307\u4EE4\u7684\u4EBA\u81EA\u5DF1\u7684\u5934\u50CF\uFF1B\u4E5F\u53EF\u4EE5\u586B\u300C\u88AB @ \u7684\u4EBA\u300D\u5217\u8868\u91CC\u7684 id\u3002\u7528\u6237\u8BF4\u300C\u753B\u6211\u300D\u300C\u7528\u6211\u7684\u5934\u50CF\u300D\u300C\u753B @\u67D0\u67D0\u300D\u65F6\u7528\u5B83\u3002",
269
+ parameters: {
270
+ type: "object",
271
+ properties: {
272
+ userId: { type: "string", description: "\u8981\u53D6\u5934\u50CF\u7684\u7528\u6237 id\uFF08\u53D6\u81EA\u4E0A\u4E0B\u6587\u91CC\u7684\u300C\u88AB @ \u7684\u4EBA\u300D\uFF09\uFF0C\u7559\u7A7A\u6216\u5199 me/self \u8868\u793A\u53D1\u6307\u4EE4\u7684\u4EBA\u81EA\u5DF1" }
273
+ },
274
+ required: []
275
+ }
276
+ }
277
+ },
261
278
  {
262
279
  type: "function",
263
280
  function: {
@@ -266,8 +283,8 @@ function buildAgentTools() {
266
283
  parameters: {
267
284
  type: "object",
268
285
  properties: {
269
- // 提示词要短:宁可朴素,也不要堆术语写成几百字的「专业」prompt
270
- prompt: { type: "string", description: "\u7ED8\u56FE\u63D0\u793A\u8BCD\u3002\u8BF4\u4EBA\u8BDD\u3001\u5199\u7B80\u77ED\u5C31\u884C\uFF0C\u4E2D\u6587\u5373\u53EF\uFF08\u7ED8\u56FE\u6A21\u578B\u542C\u5F97\u61C2\u4E2D\u6587\uFF09\uFF1B\u4E0D\u8981\u628A\u7528\u6237\u7684\u8BDD\u6269\u5199\u6210\u51E0\u767E\u5B57\uFF0C\u4E5F\u4E0D\u8981\u5806\u780C\u98CE\u683C/\u6750\u8D28/\u5149\u7EBF\u672F\u8BED\u3002\u753B\u9762\u4E0A\u5FC5\u987B\u51FA\u73B0\u7684\u6587\u5B57\u8981\u539F\u6837\u5199\u8FDB\u53BB" },
286
+ // 提示词要短;用户没说要润色就原封不动用他的原话
287
+ prompt: { type: "string", description: "\u7ED8\u56FE\u63D0\u793A\u8BCD\u3002**\u9ED8\u8BA4\u539F\u5C01\u4E0D\u52A8\u7528\u7528\u6237\u7684\u8BDD**\uFF1A\u7B80\u77ED\u3001\u8BF4\u4EBA\u8BDD\uFF0C\u4E0D\u8981\u6269\u5199\u6210\u51E0\u767E\u5B57\uFF0C\u4E0D\u8981\u8865\u753B\u98CE/\u5149\u7EBF/\u6750\u8D28\u672F\u8BED\uFF0C\u4E0D\u8981\u7FFB\u6210\u82F1\u6587\uFF1B\u53EA\u6709\u7528\u6237\u8BF4\u8FC7\u300C\u5E2E\u6211\u6DA6\u8272 / \u4F18\u5316\u4E00\u4E0B\u300D\u624D\u52A0\u5DE5\u3002\u4E2D\u6587\u5373\u53EF\uFF08\u7ED8\u56FE\u6A21\u578B\u542C\u5F97\u61C2\u4E2D\u6587\uFF09\u3002\u753B\u9762\u4E0A\u5FC5\u987B\u51FA\u73B0\u7684\u6587\u5B57\u8981\u539F\u6837\u5199\u8FDB\u53BB" },
271
288
  references: {
272
289
  type: "array",
273
290
  items: { type: "string" },
@@ -346,6 +363,53 @@ function cleanAskMessage(raw, max = 200) {
346
363
  if (text.length > max) text = text.slice(0, max) + "\u2026";
347
364
  return text;
348
365
  }
366
+ function parsePolishIntent(text) {
367
+ const t = String(text || "");
368
+ if (!t) return false;
369
+ if (/不(用|要|必)?(润色|优化|加工|改)|别(润色|优化|加工)|原样|原封不动|直接画|不用了/.test(t)) return false;
370
+ return /润色|优化一下|加工|丰富|扩写|细化|详细[一点些]|写(得)?(更)?(完整|详细)|帮我(写|改)/.test(t);
371
+ }
372
+ function extractAtIds(content, selfId) {
373
+ if (!content) return [];
374
+ const out = [];
375
+ const seen = /* @__PURE__ */ new Set();
376
+ for (const el of import_koishi.h.select(content, "at")) {
377
+ const id = String(el.attrs.id ?? el.attrs.user ?? "").trim();
378
+ if (!id || id === selfId) continue;
379
+ if (String(el.attrs.type || "") === "all") continue;
380
+ if (seen.has(id)) continue;
381
+ seen.add(id);
382
+ out.push({ id, name: String(el.attrs.name || el.attrs.nick || "").trim() });
383
+ }
384
+ return out;
385
+ }
386
+ function buildAvatarUrl(input) {
387
+ const userId = String(input?.userId || "").trim();
388
+ if (!userId) return "";
389
+ const size = Number(input.size) > 0 ? Number(input.size) : 640;
390
+ const platform = String(input.platform || "");
391
+ const appId = String(input.appId || "");
392
+ if (input.template) {
393
+ return String(input.template).replace(/\{(\w+)\}/g, (_m, key) => {
394
+ const vars = {
395
+ userId,
396
+ platform,
397
+ size: String(size),
398
+ selfId: String(input.selfId || ""),
399
+ appId
400
+ };
401
+ return vars[key] ?? "";
402
+ });
403
+ }
404
+ if (input.authorAvatar) return String(input.authorAvatar);
405
+ if (appId && (platform === "qq" || platform === "qqguild")) {
406
+ return `https://q.qlogo.cn/qqapp/${appId}/${userId}/${size}`;
407
+ }
408
+ if (/^(qq|onebot|red|qqguild)$/.test(platform) && /^\d{5,12}$/.test(userId)) {
409
+ return `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=${size}`;
410
+ }
411
+ return "";
412
+ }
349
413
  function trimGallery(records, capacity) {
350
414
  const limit = capacity > 0 ? capacity : 50;
351
415
  const byGroup = /* @__PURE__ */ new Map();
@@ -721,6 +785,7 @@ var DEFAULT_AGENT_INSTRUCTIONS = `\u4F60\u662F QQ \u7FA4\u91CC\u7684\u7ED8\u56FE
721
785
  - gallery_search\uFF1A\u6309\u5173\u952E\u8BCD\u641C\u7D22\u53C2\u8003\u56FE\u5E93\u3002**\u6D89\u53CA\u5177\u4F53\u89D2\u8272/\u7ACB\u7ED8\u65F6\u5FC5\u987B\u5148\u641C\u4E00\u6B21**\u3002
722
786
  \u591A\u4E2A\u5173\u952E\u8BCD\u7528\u7A7A\u683C\u5206\u9694\uFF08\u5982\u300C\u767D\u53D1 \u7ACB\u7ED8\u300D\uFF09\uFF0C\u5168\u90E8\u547D\u4E2D\u7684\u6392\u5728\u6700\u524D\uFF1B\u641C\u4E0D\u5230\u5C31\u6362\u4E2A\u8BCD\u518D\u641C\u3002
723
787
  - ask_user\uFF1A\u628A\u95EE\u9898\u53D1\u7ED9\u7528\u6237**\u5E76\u7B49\u4ED6\u56DE\u590D**\uFF08\u4ED6\u53D1\u7684\u56FE\u4E5F\u4F1A\u4E00\u8D77\u5E26\u56DE\u6765\uFF09\u3002
788
+ - get_avatar\uFF1A\u53D6\u67D0\u4E2A\u4EBA\u7684\u5934\u50CF\u5F53\u53C2\u8003\u56FE\u3002userId \u7559\u7A7A\u53D6\u53D1\u6307\u4EE4\u8005\u81EA\u5DF1\uFF1B\u4E5F\u53EF\u4EE5\u53D6\u88AB @ \u7684\u4EBA\u7684\u5934\u50CF\u3002
724
789
  - draw\uFF1A\u771F\u6B63\u5F00\u59CB\u753B\u3002\u628A prompt \u548C\u6311\u4E2D\u7684\u53C2\u8003\u56FE\u7F16\u53F7\u4EA4\u8FDB\u53BB\uFF0C\u753B\u597D\u4F1A\u81EA\u52A8\u53D1\u5230\u7FA4\u91CC\u3002
725
790
 
726
791
  \u5DE5\u4F5C\u89C4\u5219\uFF1A
@@ -735,25 +800,39 @@ var DEFAULT_AGENT_INSTRUCTIONS = `\u4F60\u662F QQ \u7FA4\u91CC\u7684\u7ED8\u56FE
735
800
  4. \u753B\u9762\u4E0A\u8981\u51FA\u73B0\u6587\u5B57\uFF08\u53F0\u8BCD\u3001\u6807\u9898\u3001\u62DB\u724C\u3001\u5B57\u5E55\uFF09\u65F6\uFF0C\u628A\u6587\u5B57\u539F\u6837\u5199\u8FDB prompt\uFF0C\u5E76\u8BF4\u660E\u300C\u8FD9\u4E9B\u5B57\u5FC5\u987B\u539F\u6837\u51FA\u73B0\uFF0C\u4E0D\u80FD\u53D8\u5F62\u6216\u81EA\u521B\u5B57\u5F62\u300D\u3002
736
801
  5. draw \u8FD4\u56DE ok=false \u65F6\uFF0C\u770B error \u51B3\u5B9A\uFF1A\u80FD\u6539\u7684\uFF08\u63D0\u793A\u8BCD\u95EE\u9898\uFF09\u5C31\u6362\u4E2A\u5199\u6CD5\u91CD\u8BD5\uFF0C\u6539\u4E0D\u4E86\u7684\uFF08\u9650\u6D41/\u914D\u989D\uFF09\u5C31\u5982\u5B9E\u544A\u8BC9\u7528\u6237\u3002
737
802
  6. \u6D41\u7A0B\u7ED3\u675F\u540E\u7528\u4E2D\u6587\u56DE\u4E00\u53E5\u7B80\u77ED\u7684\u7ED3\u679C\u8BF4\u660E\uFF08\u4E0D\u8981\u590D\u8FF0 prompt\uFF0C\u4E0D\u8981\u5199\u5C0F\u4F5C\u6587\uFF0C\u4E0D\u8981\u518D\u95EE\u300C\u8FD8\u8981\u6211\u505A\u4EC0\u4E48\u300D\uFF09\u3002
738
- 7. \u56FE\u7247\u7F16\u53F7\uFF1Agallery_search \u7ED3\u679C\u91CC\u7684 id\u3001\u7528\u6237\u53D1\u7684\u56FE\uFF08user1\u3001user2\u2026\uFF09\u3001\u6307\u4EE4\u9ED8\u8BA4\u56FE\uFF08cmd1\u2026\uFF09\u90FD\u53EF\u4EE5\u76F4\u63A5\u5199\u8FDB draw \u7684 references\u3002
739
- \u53EA\u586B\u4F60\u786E\u8BA4\u5B58\u5728\u7684\u7F16\u53F7\uFF0C**\u7EDD\u5BF9\u4E0D\u8981\u7F16\u9020 id**\u3002
740
- 8. \u3010prompt \u8981\u77ED\uFF0C\u8981\u8BF4\u4EBA\u8BDD\u3011**\u4E0D\u8981**\u628A\u7528\u6237\u7684\u8BDD\u6269\u5199\u6210\u51E0\u767E\u5B57\u7684\u300C\u4E13\u4E1A\u300D\u63D0\u793A\u8BCD\uFF1A
803
+ 7. \u56FE\u7247\u7F16\u53F7\uFF1Agallery_search \u7ED3\u679C\u91CC\u7684 id\u3001\u7528\u6237\u968F\u6D88\u606F\u53D1\u7684\u56FE\u3001\u5934\u50CF\u3001\u6307\u4EE4\u9ED8\u8BA4\u56FE\u90FD\u53EF\u4EE5\u76F4\u63A5\u5199\u8FDB draw \u7684 references\u3002
804
+ \u53EA\u586B\u4E0A\u4E0B\u6587\u91CC**\u660E\u786E\u5217\u51FA\u8FC7**\u7684\u7F16\u53F7\uFF0C**\u7EDD\u5BF9\u4E0D\u8981\u7F16\u9020 id**\u3002
805
+ - \u7528\u6237\u968F\u6D88\u606F\u53D1\u7684\u56FE\u4F1A\u81EA\u52A8\u5E26\u4E0A\uFF0C\u4E0D\u7528\u4F60\u518D\u586B\uFF1B
806
+ - \u4ED6 @ \u4E86\u8C01\uFF0C\u90A3\u4E2A\u4EBA\u7684\u5934\u50CF\u4E5F\u4F1A\u81EA\u52A8\u5E26\u4E0A\u3002
807
+ 8. \u3010\u6CA1\u6307\u5B9A\u53C2\u8003\u56FE\u65F6\u5148\u95EE\u4E00\u53E5\u3011\u7528\u6237\u8FD9\u6B21\u6CA1\u53D1\u56FE\u3001\u6CA1 @ \u4EBA\u3001\u6307\u4EE4\u672C\u8EAB\u4E5F\u6CA1\u6709\u9ED8\u8BA4\u53C2\u8003\u56FE\uFF0C\u4F60\u4E5F\u641C\u4E0D\u5230\u5408\u9002\u7684\u7ACB\u7ED8\u65F6\uFF0C
808
+ **\u4E0D\u8981\u76F4\u63A5 draw**\u3002\u5148\u7528 ask_user \u95EE\u4E00\u53E5\u8981\u4E0D\u8981\u53C2\u8003\u56FE\uFF0C\u5E76\u7ED9\u51FA\u9009\u9879\uFF08\u4F8B\u5982\u300C\u7528\u4F60\u81EA\u5DF1\u7684\u5934\u50CF / \u7528 @\u67D0\u67D0 \u7684\u5934\u50CF /
809
+ \u6211\u5E2E\u4F60\u4ECE\u56FE\u5E93\u627E\u4E00\u5F20 / \u4E0D\u7528\u53C2\u8003\u76F4\u63A5\u753B\u300D\uFF09\u3002\u7528\u6237\u8BF4\u300C\u4E0D\u7528 / \u968F\u4FBF / \u76F4\u63A5\u753B\u300D\u5C31\u7ACB\u523B\u5F00\u753B\uFF0C**\u4E0D\u8981\u95EE\u7B2C\u4E8C\u6B21**\u3002
810
+ 9. \u3010\u8981\u753B\u300C\u6211\u300D\u5C31\u7528\u5934\u50CF\u3011\u7528\u6237\u8BF4\u300C\u753B\u6211\u300D\u300C\u7528\u6211\u7684\u5F62\u8C61\u300D\u300C\u753B\u6211\u548C xxx\u300D\u5374\u6CA1\u7ED9\u56FE\u65F6\uFF0C\u7528 get_avatar \u53D6\u4ED6\u81EA\u5DF1\u7684\u5934\u50CF\uFF1B
811
+ \u8BF4\u300C\u753B @\u67D0\u67D0\u300D\u65F6\u53D6\u90A3\u4E2A\u4EBA\u7684\u5934\u50CF\uFF08\u4E00\u822C\u5DF2\u7ECF\u81EA\u52A8\u5E26\u4E0A\u4E86\uFF0C\u76F4\u63A5\u7528\u90A3\u4E2A\u7F16\u53F7\u5373\u53EF\uFF09\u3002
812
+ 10. \u3010\u9ED8\u8BA4\u4E0D\u6DA6\u8272\uFF0C\u7528\u6237\u70B9\u5934\u624D\u6DA6\u8272\u3011\u628A\u7528\u6237\u7684\u8BDD**\u539F\u5C01\u4E0D\u52A8**\u4EA4\u7ED9\u7ED8\u56FE\u6A21\u578B\u5C31\u662F\u6700\u597D\u7684\u9ED8\u8BA4\u503C\uFF1A
813
+ - **\u4E0D\u8981**\u4E3B\u52A8\u6269\u5199\u3001\u4E0D\u8981\u8865\u753B\u98CE/\u5149\u7EBF/\u6750\u8D28/\u955C\u5934\u3001\u4E0D\u8981\u7FFB\u6210\u82F1\u6587\u3001\u4E0D\u8981\u52A0\u300Cmasterpiece\u300D\u8FD9\u7C7B\u8BCD\uFF1B
814
+ - \u7528\u6237\u660E\u786E\u8BF4\u300C\u5E2E\u6211\u6DA6\u8272 / \u4F18\u5316\u4E00\u4E0B / \u5199\u8BE6\u7EC6\u70B9\u300D\u65F6\uFF0C\u4F60\u624D\u628A\u63D0\u793A\u8BCD\u5199\u5F97\u66F4\u5B8C\u6574\uFF1B
815
+ - draw \u5DE5\u5177\u8FD4\u56DE asked=true \u65F6\u770B\u5B83\u7ED9\u7684 polish \u5B57\u6BB5\uFF1Apolish=true \u624D\u6DA6\u8272\uFF0Cfalse \u6216\u6CA1\u7ED9\u5C31**\u4E00\u4E2A\u5B57\u90FD\u522B\u6539**\uFF1B
741
816
  - \u7528\u6237\u7528\u4E2D\u6587\u63D0\u7684\u9700\u6C42\uFF0Cprompt \u5C31\u5199\u4E2D\u6587\uFF08\u7ED8\u56FE\u6A21\u578B\u542C\u5F97\u61C2\u4E2D\u6587\uFF0C\u7406\u89E3\u529B\u4E5F\u591F\u7528\uFF09\uFF1B
742
- - \u522B\u4E3A\u4E86\u663E\u5F97\u4E13\u4E1A\u5C31\u53BB\u5806\u98CE\u683C / \u6750\u8D28 / \u5149\u7EBF / \u955C\u5934\u672F\u8BED\uFF0C\u66F4\u522B\u523B\u610F\u7FFB\u6210\u82F1\u6587\uFF1B
743
817
  - \u53EA\u4FDD\u7559\u7528\u6237\u771F\u6B63\u8BF4\u8FC7\u7684\u5185\u5BB9 + \u753B\u9762\u5FC5\u987B\u6709\u7684\u8981\u7D20\uFF0C**\u4E0D\u8981\u66FF\u4ED6\u8111\u8865\u7EC6\u8282**\uFF1B
744
818
  - \u63D0\u793A\u8BCD\u8D8A\u957F\uFF0C\u5404\u8981\u7D20\u4E92\u76F8\u7A00\u91CA\uFF0C\u51FA\u56FE\u53CD\u800C\u8D8A\u8D70\u6837\u3002\u4E00\u4E24\u53E5\u8BDD\u80FD\u628A\u753B\u9762\u8BF4\u6E05\u5C31\u591F\u4E86\u3002
745
819
 
746
820
  \u5982\u679C\u4F60\u6240\u5728\u7684\u63A5\u53E3\u4E0D\u652F\u6301\u51FD\u6570\u8C03\u7528\uFF0C\u5C31**\u53EA\u8F93\u51FA\u4E00\u884C JSON** \u6765\u8C03\u7528\u5DE5\u5177\uFF0C\u4F8B\u5982\uFF1A
747
821
  {"tool":"gallery_search","args":{"keyword":"\u767D\u53D1 \u7ACB\u7ED8"}}
748
822
  {"tool":"ask_user","args":{"question":"\u60F3\u8981\u4EC0\u4E48\u753B\u98CE\uFF1F"}}
823
+ {"tool":"get_avatar","args":{"userId":""}}
749
824
  {"tool":"draw","args":{"prompt":"...","references":["ref1","user1"]}}
750
825
  \u4E0D\u60F3\u8C03\u7528\u5DE5\u5177\u65F6\uFF0C\u6B63\u5E38\u8F93\u51FA\u7ED9\u7528\u6237\u770B\u7684\u4E2D\u6587\u5373\u53EF\u3002`;
751
- var AGENT_CONFIRM_RULE_ON = `**\u5FC5\u987B\u5148 ask_user**\uFF1A\u7528\u4E00\u53E5\u8BDD\u544A\u8BC9\u7528\u6237\u300C\u8FD9\u6B21\u4F1A\u53C2\u8003\u54EA\u51E0\u5F20\u56FE + \u5927\u81F4\u753B\u6210\u4EC0\u4E48\u6837\u300D\uFF0C\u7528\u6237\u70B9\u5934\u540E\u624D\u80FD\u8C03\u7528 draw\u3002
826
+ var AGENT_CONFIRM_RULE_ON = `**\u5FC5\u987B\u5148 ask_user**\uFF1A\u7528\u4E00\u53E5\u8BDD\u544A\u8BC9\u7528\u6237\u300C\u8FD9\u6B21\u4F1A\u53C2\u8003\u54EA\u51E0\u5F20\u56FE + \u5927\u81F4\u753B\u6210\u4EC0\u4E48\u6837\u300D\uFF0C
827
+ \u5E76\u987A\u5E26\u95EE\u4E00\u53E5\u300C\u63D0\u793A\u8BCD\u8981\u4E0D\u8981\u6211\u5E2E\u4F60\u6DA6\u8272\uFF08\u9ED8\u8BA4\u76F4\u63A5\u7528\u4F60\u7684\u539F\u8BDD\uFF09\u300D\uFF0C\u7528\u6237\u70B9\u5934\u540E\u624D\u80FD\u8C03\u7528 draw\u3002
828
+ - \u7528\u6237\u8BF4\u300C\u6DA6\u8272 / \u4F18\u5316 / \u4E30\u5BCC\u4E00\u4E0B\u300D\u2192 \u4F60\u518D\u628A\u63D0\u793A\u8BCD\u5199\u5F97\u66F4\u5B8C\u6574\uFF1B
829
+ - \u7528\u6237\u8BF4\u300C\u76F4\u63A5\u753B / \u4E0D\u7528 / \u539F\u6837\u300D\u6216\u6CA1\u63D0\u8FD9\u4EF6\u4E8B \u2192 **\u539F\u5C01\u4E0D\u52A8\u7528\u4ED6\u7684\u539F\u8BDD**\uFF0C\u4E00\u4E2A\u5B57\u90FD\u522B\u6DA6\u8272\u3002
752
830
  \u7528\u6237\u56DE\u590D\u4E86\u5426\u5B9A\u6216\u63D0\u51FA\u4FEE\u6539\u610F\u89C1\uFF0C\u5C31\u6309\u4ED6\u8BF4\u7684\u6539\uFF0C\u518D\u786E\u8BA4\u4E00\u6B21\u6216\u76F4\u63A5\u753B\uFF08\u4ED6\u8BF4\u300C\u76F4\u63A5\u753B\u300D\u5C31\u4E0D\u7528\u518D\u95EE\uFF09\u3002`;
753
831
  var AGENT_CONFIRM_RULE_OFF = `\u7528\u6237\u9700\u6C42\u5DF2\u7ECF\u660E\u786E\u7684\uFF0C\u76F4\u63A5\u8C03\u7528 draw\uFF0C\u4E0D\u7528\u591A\u95EE\u4E00\u53E5\u3002\u53EA\u6709\u771F\u7684\u7F3A\u5173\u952E\u4FE1\u606F\u624D ask_user\u3002`;
754
832
  var AGENT_JSON_FALLBACK_RULE = `\uFF08\u6CE8\u610F\uFF1A\u5F53\u524D\u63A5\u53E3\u4E0D\u652F\u6301\u51FD\u6570\u8C03\u7528\uFF09\u8BF7\u4F60**\u53EA\u7528\u4E00\u884C JSON** \u6765\u8C03\u7528\u5DE5\u5177\uFF0C\u4E0D\u8981\u8F93\u51FA\u522B\u7684\u5185\u5BB9\uFF1A
755
833
  {"tool":"gallery_search","args":{"keyword":"\u767D\u53D1 \u7ACB\u7ED8"}}
756
834
  {"tool":"ask_user","args":{"question":"\u60F3\u8981\u4EC0\u4E48\u753B\u98CE\uFF1F"}}
835
+ {"tool":"get_avatar","args":{"userId":""}}
757
836
  {"tool":"draw","args":{"prompt":"...","references":["ref1","ref2"]}}
758
837
  \u4E0D\u9700\u8981\u8C03\u7528\u5DE5\u5177\u65F6\uFF0C\u6B63\u5E38\u8F93\u51FA\u7ED9\u7528\u6237\u770B\u7684\u4E2D\u6587\u5373\u53EF\u3002`;
759
838
  var defaultCommands = [
@@ -1299,9 +1378,20 @@ var Config = import_koishi.Schema.intersect([
1299
1378
  model: import_koishi.Schema.string().description("agent \u4E13\u7528\u7684\u6A21\u578B\uFF08\u7559\u7A7A\u5219\u7528\u300CAI \u6A21\u578B\u63A5\u53E3\u300D\u91CC\u7684\u6A21\u578B\uFF09\u3002**\u9700\u8981\u652F\u6301 function calling**\uFF1B\u4E0D\u652F\u6301\u65F6\u63D2\u4EF6\u4F1A\u81EA\u52A8\u6539\u7528\u300C\u4E00\u884C JSON\u300D\u534F\u8BAE"),
1300
1379
  baseUrl: import_koishi.Schema.string().role("link").description("agent \u4E13\u7528\u63A5\u53E3\u5730\u5740\uFF08\u7559\u7A7A\u5219\u590D\u7528\u300CAI \u6A21\u578B\u63A5\u53E3\u300D\u7684\u5730\u5740\uFF09"),
1301
1380
  apiKey: import_koishi.Schema.string().role("secret").description("agent \u4E13\u7528\u5BC6\u94A5\uFF08\u7559\u7A7A\u5219\u590D\u7528\u300CAI \u6A21\u578B\u63A5\u53E3\u300D\u7684\u5BC6\u94A5\uFF09"),
1302
- instructions: import_koishi.Schema.string().role("textarea", { rows: [16, 8] }).default(DEFAULT_AGENT_INSTRUCTIONS).description("\u9A71\u52A8 agent \u884C\u4E3A\u7684\u7CFB\u7EDF\u63D0\u793A\u8BCD\uFF08\u53EA\u5199\u300C\u6709\u54EA\u4E9B\u5DE5\u5177 + \u4EC0\u4E48\u65F6\u5019\u7528\u300D\uFF0C\u522B\u5199\u6B7B\u6D41\u7A0B\uFF09\u3002\u53EF\u7528\u5360\u4F4D\u7B26\uFF1A{confirmRule} \u5F00\u753B\u786E\u8BA4\u89C4\u5219\u3001{command} \u6307\u4EE4\u540D\u3001{prompt} \u6307\u4EE4\u63D0\u793A\u8BCD\u3001{userInput} \u7528\u6237\u9644\u52A0\u9700\u6C42\u3001{imageCount} \u7528\u6237\u968F\u6D88\u606F\u53D1\u7684\u56FE\u6570\u91CF\u3001{refCount} \u53EF\u68C0\u7D22\u7684\u53C2\u8003\u56FE\u6570\u91CF\u3002\u6307\u4EE4\u540D/\u63D0\u793A\u8BCD\u7B49\u4E0A\u4E0B\u6587\u4F1A\u81EA\u52A8\u9644\u5728\u63D0\u793A\u8BCD\u672B\u5C3E\uFF0C\u4E0D\u7528\u81EA\u5DF1\u5F15\u7528")
1381
+ instructions: import_koishi.Schema.string().role("textarea", { rows: [16, 8] }).default(DEFAULT_AGENT_INSTRUCTIONS).description("\u9A71\u52A8 agent \u884C\u4E3A\u7684\u7CFB\u7EDF\u63D0\u793A\u8BCD\uFF08\u53EA\u5199\u300C\u6709\u54EA\u4E9B\u5DE5\u5177 + \u4EC0\u4E48\u65F6\u5019\u7528\u300D\uFF0C\u522B\u5199\u6B7B\u6D41\u7A0B\uFF09\u3002\u53EF\u7528\u5360\u4F4D\u7B26\uFF1A{confirmRule} \u5F00\u753B\u786E\u8BA4\u89C4\u5219\u3001{command} \u6307\u4EE4\u540D\u3001{prompt} \u6307\u4EE4\u63D0\u793A\u8BCD\u3001{userInput} \u7528\u6237\u9644\u52A0\u9700\u6C42\u3001{imageCount} \u7528\u6237\u968F\u6D88\u606F\u53D1\u7684\u56FE\u6570\u91CF\u3001{refCount} \u53EF\u68C0\u7D22\u7684\u53C2\u8003\u56FE\u6570\u91CF\u3002\u6307\u4EE4\u540D/\u63D0\u793A\u8BCD\u7B49\u4E0A\u4E0B\u6587\u4F1A\u81EA\u52A8\u9644\u5728\u63D0\u793A\u8BCD\u672B\u5C3E\uFF0C\u4E0D\u7528\u81EA\u5DF1\u5F15\u7528"),
1382
+ askIfNoReference: import_koishi.Schema.boolean().default(true).description("\u7528\u6237\u8FD9\u6B21\u65E2\u6CA1\u53D1\u56FE\u3001\u4E5F\u6CA1 @ \u4EBA\u3001\u6307\u4EE4\u672C\u8EAB\u4E5F\u6CA1\u6709\u9ED8\u8BA4\u53C2\u8003\u56FE\u65F6\uFF0C\u5F00\u753B\u524D\u5148\u95EE\u4E00\u53E5\u300C\u8981\u4E0D\u8981\u53C2\u8003\u56FE\u300D\uFF08\u53EF\u4EE5\u7528\u4ED6\u81EA\u5DF1\u7684\u5934\u50CF / @ \u522B\u4EBA\u7684\u5934\u50CF\uFF09\u3002\u8FD9\u6761\u662F\u63D2\u4EF6\u515C\u5E95\u6267\u884C\u7684\uFF0C\u6A21\u578B\u81EA\u5DF1\u5148\u95EE\u8FC7\u5C31\u4E0D\u518D\u95EE"),
1383
+ askBeforePolish: import_koishi.Schema.boolean().default(true).description("\u5F00\u753B\u524D\u5148\u95EE\u7528\u6237\u300C\u63D0\u793A\u8BCD\u8981\u4E0D\u8981\u6211\u5E2E\u4F60\u6DA6\u8272\u300D\u3002**\u9ED8\u8BA4\u662F\u539F\u5C01\u4E0D\u52A8\u628A\u7528\u6237\u7684\u539F\u8BDD\u4EA4\u7ED9\u7ED8\u56FE\u6A21\u578B**\uFF0C\u53EA\u6709\u7528\u6237\u8BF4\u300C\u6DA6\u8272 / \u4F18\u5316\u300D\u624D\u52A0\u5DE5\u3002\u8FD9\u6761\u540C\u6837\u662F\u63D2\u4EF6\u515C\u5E95\uFF0C\u6A21\u578B\u81EA\u5DF1\u5148\u95EE\u8FC7\u5C31\u4E0D\u518D\u95EE")
1303
1384
  }).collapse().description("Agent \u914D\u7F6E\uFF08\u6307\u4EE4\u8F83\u957F\uFF0C\u5DF2\u6298\u53E0\uFF09")
1304
1385
  }).description("Agent \u6A21\u5F0F"),
1386
+ import_koishi.Schema.object({
1387
+ avatar: import_koishi.Schema.object({
1388
+ enabled: import_koishi.Schema.boolean().default(true).description("\u5F00\u542F\u5934\u50CF\u53C2\u8003\u56FE\uFF1A\u53EF\u4EE5\u628A\u7FA4\u53CB\u7684\u5934\u50CF\u5F53\u53C2\u8003\u56FE\u6765\u753B"),
1389
+ autoAt: import_koishi.Schema.boolean().default(true).description("\u7528\u6237 @ \u4E86\u8C01\uFF0C\u5C31\u81EA\u52A8\u628A\u90A3\u4E2A\u4EBA\u7684\u5934\u50CF\u4F5C\u4E3A\u53C2\u8003\u56FE\uFF08@ \u672C\u8EAB\u5C31\u662F\u660E\u786E\u7684\u6307\u5B9A\uFF09"),
1390
+ autoSelf: import_koishi.Schema.boolean().default(false).description("\u7528\u6237\u6CA1\u53D1\u56FE\u4E5F\u6CA1 @ \u4EBA\u65F6\uFF0C\u81EA\u52A8\u62FF\u4ED6\u81EA\u5DF1\u7684\u5934\u50CF\u5F53\u53C2\u8003\u56FE\u3002\u5173\u6389\u4E5F\u6CA1\u5173\u7CFB\uFF0C\u300C\u7528\u6211\u7684\u5934\u50CF\u753B\u300D\u8FD9\u7C7B\u9700\u6C42 agent \u4F1A\u81EA\u5DF1\u8C03\u7528 get_avatar"),
1391
+ size: import_koishi.Schema.number().default(640).min(64).max(1024).step(64).description("\u5934\u50CF\u8FB9\u957F\uFF08\u50CF\u7D20\uFF09"),
1392
+ urlTemplate: import_koishi.Schema.string().default("").description("\u81EA\u5B9A\u4E49\u5934\u50CF\u5730\u5740\u6A21\u677F\uFF0C\u7559\u7A7A\u7528\u5185\u7F6E\u89C4\u5219\u3002\u5360\u4F4D\u7B26\uFF1A{userId} \u7528\u6237 id\u3001{platform} \u5E73\u53F0\u3001{selfId} \u673A\u5668\u4EBA id\u3001{appId} \u673A\u5668\u4EBA appid\u3001{size} \u5C3A\u5BF8")
1393
+ }).description("\u5934\u50CF\u53C2\u8003\u56FE")
1394
+ }).description("\u5934\u50CF\u53C2\u8003\u56FE"),
1305
1395
  import_koishi.Schema.object({
1306
1396
  appendUserInput: import_koishi.Schema.boolean().default(true).description("\u628A\u7528\u6237\u968F\u6307\u4EE4\u53D1\u7684\u9644\u52A0\u9700\u6C42\u5E76\u5165\u7ED8\u56FE\u63D0\u793A\u8BCD\uFF08\u4F8B\u5982\u300C\u624B\u529E\u5316 xxx \u5728\u5077\u5403\u767D\u996D\u88AB\u53D1\u73B0\u7684\u8868\u60C5\u300D\uFF09"),
1307
1397
  promptOptimize: import_koishi.Schema.union([
@@ -1470,6 +1560,8 @@ function apply(ctx, config) {
1470
1560
  needimages: "\u8BF7\u63D0\u4F9B\u81F3\u5C11\u4E00\u5F20\u56FE\u7247",
1471
1561
  agentfailed: "\u7ED8\u56FE\u52A9\u624B\u8FD9\u6B21\u6CA1\u8DD1\u8D77\u6765\uFF08{0}\uFF09\u3002\u53EF\u4EE5\u5148\u5173\u6389\u300CAgent \u6A21\u5F0F\u300D\u7167\u5E38\u753B\u56FE\uFF0C\u6216\u6362\u4E00\u4E2A\u652F\u6301\u5DE5\u5177\u8C03\u7528\u7684\u6A21\u578B\u3002",
1472
1562
  agentnoresult: "\u8FD9\u6B21\u6CA1\u80FD\u7406\u51FA\u4E2A\u7ED3\u679C\u6765\uFF0C\u9EBB\u70E6\u518D\u8BF4\u4E00\u6B21\u4F60\u60F3\u753B\u4EC0\u4E48\uFF5E",
1563
+ askreference: "\u8FD9\u6B21\u6CA1\u6709\u6307\u5B9A\u53C2\u8003\u56FE\u3002\u8981\u53C2\u8003\u56FE\u7247\u5417\uFF1F\u53EF\u4EE5\u76F4\u63A5\u53D1\u56FE\u3001@ \u4E00\u4E2A\u4EBA\u7528\u4ED6\u7684\u5934\u50CF\uFF0C\u6216\u8005\u8BF4\u300C\u7528\u6211\u7684\u5934\u50CF\u300D\uFF1B\u4E0D\u9700\u8981\u5C31\u56DE\u300C\u76F4\u63A5\u753B\u300D\u3002\n\u53E6\u5916\uFF0C\u63D0\u793A\u8BCD\u8981\u6211\u5E2E\u4F60\u6DA6\u8272\u4E00\u4E0B\u5417\uFF1F\u9ED8\u8BA4\u76F4\u63A5\u7528\u4F60\u7684\u539F\u8BDD\u3002",
1564
+ askpolish: "\u8FD9\u6B21\u76F4\u63A5\u6309\u4F60\u7684\u539F\u8BDD\u5F00\u753B\u3002\u8981\u6211\u5148\u5E2E\u4F60\u6DA6\u8272\u4E00\u4E0B\u63D0\u793A\u8BCD\u5417\uFF1F\u56DE\u300C\u6DA6\u8272\u300D\u6211\u52A0\u5DE5\u4E00\u4E0B\uFF0C\u56DE\u300C\u76F4\u63A5\u753B\u300D\u5C31\u539F\u6837\u4EA4\u7ED9\u7ED8\u56FE\u6A21\u578B\u3002",
1473
1565
  optimizefailed: "\uFF08\u63D0\u793A\u8BCD\u878D\u5408\u5931\u8D25\uFF0C\u5DF2\u6539\u4E3A\u8FFD\u52A0\u6A21\u5F0F\uFF09",
1474
1566
  bgstart: "\u5DF2\u5F00\u59CB\u540E\u53F0\u7ED8\u56FE\uFF08{0} \u5F20\u56FE\uFF09\uFF0C\u753B\u597D\u540E\u4F1A\u5728\u8FD9\u91CC\u901A\u77E5\u4F60...",
1475
1567
  bgqueue: "\uFF08\u5F53\u524D\u6709 {0} \u4E2A\u4EFB\u52A1\u5728\u6392\u961F\uFF09",
@@ -1538,7 +1630,28 @@ function apply(ctx, config) {
1538
1630
  for (const item of pool) register("gallery", item.url, item.description, item.group);
1539
1631
  const baseIds = (defaultImageUrls || []).filter(Boolean).map((url) => register("cmd", url, "", "\u6307\u4EE4\u9ED8\u8BA4\u56FE"));
1540
1632
  const userIds = extractImagesFromSession(session).map((url) => register("user", url, "", "\u7528\u6237\u53D1\u9001"));
1541
- logInfo2(`[${cmdConfig.name}] agent \u542F\u52A8\uFF1A\u53EF\u641C\u7D22\u53C2\u8003\u56FE ${pool.length} \u5F20\uFF0C\u9ED8\u8BA4\u56FE ${baseIds.length} \u5F20\uFF0C\u7528\u6237\u968F\u6D88\u606F\u53D1\u7684\u56FE ${userIds.length} \u5F20`);
1633
+ const avatarCfg = config.avatar || {};
1634
+ const avatarOn = avatarCfg.enabled !== false;
1635
+ const atTargets = avatarOn ? collectAtTargets(session) : [];
1636
+ let selfAvatarId = "";
1637
+ const atAvatarIds = [];
1638
+ const atAvatarById = /* @__PURE__ */ new Map();
1639
+ if (avatarOn) {
1640
+ const selfUrl = await fetchAvatarUrl(session, session.userId);
1641
+ if (selfUrl) {
1642
+ selfAvatarId = register("avatar", selfUrl, "\u53D1\u6307\u4EE4\u7684\u4EBA\u7684\u5934\u50CF", "\u5934\u50CF");
1643
+ }
1644
+ if (avatarCfg.autoAt !== false) {
1645
+ for (const target of atTargets) {
1646
+ const url = await fetchAvatarUrl(session, target.id);
1647
+ if (!url) continue;
1648
+ const id = register("avatar", url, `${target.name || target.id} \u7684\u5934\u50CF`, "\u5934\u50CF");
1649
+ if (!atAvatarIds.includes(id)) atAvatarIds.push(id);
1650
+ atAvatarById.set(target.id, id);
1651
+ }
1652
+ }
1653
+ }
1654
+ logInfo2(`[${cmdConfig.name}] agent \u542F\u52A8\uFF1A\u53EF\u641C\u7D22\u53C2\u8003\u56FE ${pool.length} \u5F20\uFF0C\u9ED8\u8BA4\u56FE ${baseIds.length} \u5F20\uFF0C\u7528\u6237\u968F\u6D88\u606F\u53D1\u7684\u56FE ${userIds.length} \u5F20\uFF0C\u5934\u50CF ${atAvatarIds.length + (selfAvatarId ? 1 : 0)} \u5F20`);
1542
1655
  const historyKey = agentHistoryKey(session);
1543
1656
  const history = historyTurns > 0 ? (agentHistories.get(historyKey) || []).slice() : [];
1544
1657
  const newHistory = [];
@@ -1547,7 +1660,13 @@ function apply(ctx, config) {
1547
1660
  };
1548
1661
  remember({ role: "user", content: userInputText || "\uFF08\u7528\u6237\u6CA1\u6709\u9644\u52A0\u8BF4\u660E\uFF09" });
1549
1662
  const messages = [
1550
- { role: "system", content: buildAgentSystemPrompt(cmdConfig, userInputText, userIds.length, pool.length, baseIds, agentCfg) },
1663
+ { role: "system", content: buildAgentSystemPrompt(cmdConfig, userInputText, pool.length, {
1664
+ userIds,
1665
+ baseIds,
1666
+ atAvatarIds,
1667
+ selfAvatarId,
1668
+ atTargets
1669
+ }, agentCfg) },
1551
1670
  ...history,
1552
1671
  { role: "user", content: userInputText ? userInputText : "\uFF08\u7528\u6237\u6CA1\u6709\u9644\u52A0\u8BF4\u660E\uFF0C\u8BF7\u6309\u6307\u4EE4\u81EA\u5E26\u7684\u63D0\u793A\u8BCD\u6765\uFF09" }
1553
1672
  ];
@@ -1557,6 +1676,8 @@ function apply(ctx, config) {
1557
1676
  let finalText = "";
1558
1677
  let drew = false;
1559
1678
  let failed = "";
1679
+ let askedOnce = false;
1680
+ let drawAsked = false;
1560
1681
  for (let iteration = 0; iteration < maxIterations; iteration++) {
1561
1682
  if (!isActive || !ctx.scope.isActive) break;
1562
1683
  const res = await requestAgentChat(messages, toolsSupported ? tools : void 0);
@@ -1607,10 +1728,12 @@ ${output}` }
1607
1728
  return toolGallerySearch(args);
1608
1729
  case "ask_user":
1609
1730
  return toolAskUser(args);
1731
+ case "get_avatar":
1732
+ return toolGetAvatar(args);
1610
1733
  case "draw":
1611
1734
  return toolDraw(args);
1612
1735
  default:
1613
- return JSON.stringify({ ok: false, error: `\u6CA1\u6709\u53EB\u300C${name2}\u300D\u7684\u5DE5\u5177\uFF0C\u53EF\u7528\u7684\u662F gallery_search / ask_user / draw` });
1736
+ return JSON.stringify({ ok: false, error: `\u6CA1\u6709\u53EB\u300C${name2}\u300D\u7684\u5DE5\u5177\uFF0C\u53EF\u7528\u7684\u662F gallery_search / ask_user / get_avatar / draw` });
1614
1737
  }
1615
1738
  }
1616
1739
  function toolGallerySearch(args) {
@@ -1639,9 +1762,30 @@ ${output}` }
1639
1762
  }))
1640
1763
  });
1641
1764
  }
1765
+ async function toolGetAvatar(args) {
1766
+ if (!avatarOn) return JSON.stringify({ ok: false, error: "\u5934\u50CF\u53C2\u8003\u56FE\u529F\u80FD\u5DF2\u5173\u95ED" });
1767
+ const raw = String(args?.userId ?? args?.id ?? args?.user ?? "").trim();
1768
+ if (!raw || /^(me|self|自己|我)$/i.test(raw) || raw === session.userId) {
1769
+ if (selfAvatarId) {
1770
+ return JSON.stringify({ ok: true, id: selfAvatarId, userId: session.userId, note: "\u53D1\u6307\u4EE4\u7684\u4EBA\u81EA\u5DF1\u7684\u5934\u50CF" });
1771
+ }
1772
+ const url2 = await fetchAvatarUrl(session, session.userId);
1773
+ if (!url2) return JSON.stringify({ ok: false, error: "\u6CA1\u53D6\u5230\u4ED6\u81EA\u5DF1\u7684\u5934\u50CF\uFF08\u8FD9\u4E2A\u5E73\u53F0\u53EF\u80FD\u4E0D\u63D0\u4F9B\u5934\u50CF\u5730\u5740\uFF09" });
1774
+ selfAvatarId = register("avatar", url2, "\u53D1\u6307\u4EE4\u7684\u4EBA\u7684\u5934\u50CF", "\u5934\u50CF");
1775
+ return JSON.stringify({ ok: true, id: selfAvatarId, userId: session.userId, note: "\u53D1\u6307\u4EE4\u7684\u4EBA\u81EA\u5DF1\u7684\u5934\u50CF" });
1776
+ }
1777
+ const cached = atAvatarById.get(raw);
1778
+ if (cached) return JSON.stringify({ ok: true, id: cached, userId: raw, note: "\u88AB @ \u7684\u4EBA\u7684\u5934\u50CF" });
1779
+ const url = await fetchAvatarUrl(session, raw);
1780
+ if (!url) return JSON.stringify({ ok: false, error: `\u6CA1\u53D6\u5230 ${raw} \u7684\u5934\u50CF` });
1781
+ const id = register("avatar", url, `${raw} \u7684\u5934\u50CF`, "\u5934\u50CF");
1782
+ atAvatarById.set(raw, id);
1783
+ return JSON.stringify({ ok: true, id, userId: raw, note: "\u8FD9\u4E2A\u4EBA\u7684\u5934\u50CF" });
1784
+ }
1642
1785
  async function toolAskUser(args) {
1643
1786
  const question = cleanAskMessage(args?.question ?? args?.text ?? args?.message);
1644
1787
  if (!question) return JSON.stringify({ ok: false, error: "question \u4E0D\u80FD\u4E3A\u7A7A" });
1788
+ askedOnce = true;
1645
1789
  try {
1646
1790
  await sendNotice(session, question);
1647
1791
  } catch (error) {
@@ -1666,10 +1810,31 @@ ${output}` }
1666
1810
  const ids = images.map((url) => register("user", url, "", "\u7528\u6237\u53D1\u9001"));
1667
1811
  remember({ role: "user", content: text || (ids.length ? `\uFF08\u7528\u6237\u53D1\u4E86 ${ids.length} \u5F20\u56FE\u7247\uFF09` : "\uFF08\u7528\u6237\u4EC0\u4E48\u90FD\u6CA1\u8BF4\uFF09") });
1668
1812
  logInfo2(`agent \u63D0\u95EE\u300C${question}\u300D-> \u7528\u6237\u56DE\u590D: ${text || "\uFF08\u65E0\u6587\u5B57\uFF09"}\uFF0C\u56FE\u7247 ${ids.length} \u5F20`);
1813
+ const avatars = [];
1814
+ if (avatarOn) {
1815
+ if (avatarCfg.autoAt !== false) {
1816
+ for (const target of extractAtIds(String(answer || ""), session.selfId)) {
1817
+ const cached = atAvatarById.get(target.id);
1818
+ if (cached) {
1819
+ if (!avatars.some((a) => a.id === cached)) avatars.push({ id: cached, note: `${target.name || target.id} \u7684\u5934\u50CF` });
1820
+ continue;
1821
+ }
1822
+ const url = await fetchAvatarUrl(session, target.id);
1823
+ if (!url) continue;
1824
+ const id = register("avatar", url, `${target.name || target.id} \u7684\u5934\u50CF`, "\u5934\u50CF");
1825
+ atAvatarById.set(target.id, id);
1826
+ avatars.push({ id, note: `${target.name || target.id} \u7684\u5934\u50CF` });
1827
+ }
1828
+ }
1829
+ if (/头像|我的形象|画我|我自己/.test(text) && selfAvatarId) {
1830
+ if (!avatars.some((a) => a.id === selfAvatarId)) avatars.push({ id: selfAvatarId, note: "\u53D1\u6307\u4EE4\u7684\u4EBA\u81EA\u5DF1\u7684\u5934\u50CF" });
1831
+ }
1832
+ }
1669
1833
  return JSON.stringify({
1670
1834
  ok: true,
1671
1835
  text,
1672
- images: ids.map((id, i) => ({ id, note: `\u7528\u6237\u8FD9\u6B21\u53D1\u7684\u7B2C ${i + 1} \u5F20\u56FE` }))
1836
+ images: ids.map((id, i) => ({ id, note: `\u7528\u6237\u8FD9\u6B21\u53D1\u7684\u7B2C ${i + 1} \u5F20\u56FE` })),
1837
+ ...avatars.length ? { avatars } : {}
1673
1838
  });
1674
1839
  }
1675
1840
  async function toolDraw(args) {
@@ -1682,9 +1847,35 @@ ${output}` }
1682
1847
  if (id && !wanted.includes(id)) wanted.push(id);
1683
1848
  };
1684
1849
  baseIds.forEach(pushId);
1850
+ userIds.forEach(pushId);
1851
+ atAvatarIds.forEach(pushId);
1852
+ if (avatarOn && avatarCfg.autoSelf) pushId(selfAvatarId);
1685
1853
  ids.forEach(pushId);
1686
1854
  const capped = wanted.slice(0, maxSelect);
1687
1855
  const imageUrls = capped.map((id) => registry.get(id)?.url).filter(Boolean);
1856
+ const needRef = !capped.length && agentCfg.askIfNoReference !== false;
1857
+ const needPolish = agentCfg.askBeforePolish !== false;
1858
+ if (!drawAsked && !askedOnce && (needRef || needPolish)) {
1859
+ drawAsked = true;
1860
+ const asked = await toolAskUser({
1861
+ question: needRef ? session.text("image-prompt.messages.askreference") : session.text("image-prompt.messages.askpolish")
1862
+ });
1863
+ let parsed = {};
1864
+ try {
1865
+ parsed = JSON.parse(asked);
1866
+ } catch {
1867
+ parsed = {};
1868
+ }
1869
+ const polish = parsed.ok ? parsePolishIntent(parsed.text) : false;
1870
+ return JSON.stringify({
1871
+ ok: false,
1872
+ asked: true,
1873
+ polish,
1874
+ ...parsed.images?.length ? { newImages: parsed.images } : {},
1875
+ ...parsed.avatars?.length ? { newAvatars: parsed.avatars } : {},
1876
+ note: polish ? "\u5DF2\u7ECF\u66FF\u4F60\u95EE\u8FC7\u7528\u6237\u4E86\uFF1A\u4ED6\u8BF4\u8981\u6DA6\u8272\uFF0C\u53EF\u4EE5\u628A\u63D0\u793A\u8BCD\u5199\u5F97\u66F4\u5B8C\u6574\u3002\u4ED6\u7ED9\u4E86\u56FE/\u5934\u50CF\u4E5F\u8981\u5199\u8FDB references\u3002\u91CD\u65B0\u8C03\u7528 draw \u5F00\u753B\uFF0C\u4E0D\u8981\u518D\u95EE\u7B2C\u4E8C\u6B21" : "\u5DF2\u7ECF\u66FF\u4F60\u95EE\u8FC7\u7528\u6237\u4E86\uFF1A\u63D0\u793A\u8BCD**\u539F\u5C01\u4E0D\u52A8\u7528\u4ED6\u7684\u539F\u8BDD**\uFF0C\u4E0D\u8981\u6DA6\u8272\u3001\u4E0D\u8981\u6269\u5199\u3001\u4E0D\u8981\u7FFB\u8BD1\u3002\u4ED6\u7ED9\u4E86\u56FE/\u5934\u50CF\u5C31\u5199\u8FDB references\u3002\u91CD\u65B0\u8C03\u7528 draw \u5F00\u753B\uFF0C\u4E0D\u8981\u518D\u95EE\u7B2C\u4E8C\u6B21"
1877
+ });
1878
+ }
1688
1879
  const finalPrompt = args?.negative_prompt ? `${prompt}
1689
1880
 
1690
1881
  Negative prompt: ${String(args.negative_prompt).trim()}` : prompt;
@@ -1701,12 +1892,12 @@ Negative prompt: ${String(args.negative_prompt).trim()}` : prompt;
1701
1892
  });
1702
1893
  }
1703
1894
  }
1704
- function buildAgentSystemPrompt(cmdConfig, userInputText, imageCount, refCount, baseIds, agentCfg) {
1895
+ function buildAgentSystemPrompt(cmdConfig, userInputText, refCount, refs, agentCfg) {
1705
1896
  const instructions = renderAgentInstructions(agentCfg.instructions || DEFAULT_AGENT_INSTRUCTIONS, {
1706
1897
  command: cmdConfig.name || "",
1707
1898
  prompt: cmdConfig.prompt || "",
1708
1899
  userInput: userInputText || "",
1709
- imageCount,
1900
+ imageCount: refs.userIds.length,
1710
1901
  refCount,
1711
1902
  confirmRule: agentCfg.confirmBeforeDraw !== false ? AGENT_CONFIRM_RULE_ON : AGENT_CONFIRM_RULE_OFF
1712
1903
  });
@@ -1716,10 +1907,17 @@ Negative prompt: ${String(args.negative_prompt).trim()}` : prompt;
1716
1907
  `- \u7ED8\u56FE\u6307\u4EE4\uFF1A${cmdConfig.name}`,
1717
1908
  `- \u6307\u4EE4\u81EA\u5E26\u7684\u63D0\u793A\u8BCD\uFF1A${cmdConfig.prompt || "\uFF08\u7A7A\uFF0C\u8FD9\u662F\u81EA\u5B9A\u4E49\u6307\u4EE4\uFF09"}`,
1718
1909
  `- \u7528\u6237\u8FD9\u6B21\u8BF4\u7684\u8BDD\uFF1A${userInputText || "\uFF08\u7528\u6237\u6CA1\u8BF4\u8BDD\uFF09"}`,
1719
- `- \u7528\u6237\u968F\u6D88\u606F\u53D1\u7684\u56FE\uFF1A${imageCount} \u5F20${imageCount ? "\uFF08\u7F16\u53F7\u89C1\u4E0B\u65B9\uFF0C\u53EF\u4EE5\u76F4\u63A5\u5199\u8FDB draw \u7684 references\uFF09" : ""}`,
1910
+ `- \u7528\u6237\u968F\u6D88\u606F\u53D1\u7684\u56FE\uFF1A${refs.userIds.length ? `${refs.userIds.join("\u3001")}\uFF08\u5DF2\u81EA\u52A8\u5E26\u4E0A\uFF0C\u4E0D\u7528\u518D\u586B\uFF09` : "0 \u5F20"}`,
1720
1911
  `- \u56FE\u5E93\u91CC\u53EF\u4EE5\u641C\u5230\u7684\u53C2\u8003\u56FE\uFF1A${refCount} \u5F20`
1721
1912
  ];
1722
- if (baseIds.length) lines.push(`- \u6307\u4EE4\u9ED8\u8BA4\u53C2\u8003\u56FE\uFF08\u5DF2\u81EA\u52A8\u5E26\u4E0A\uFF0C\u4E0D\u7528\u518D\u586B\uFF09\uFF1A${baseIds.join("\u3001")}`);
1913
+ if (refs.baseIds.length) lines.push(`- \u6307\u4EE4\u9ED8\u8BA4\u53C2\u8003\u56FE\uFF08\u5DF2\u81EA\u52A8\u5E26\u4E0A\uFF0C\u4E0D\u7528\u518D\u586B\uFF09\uFF1A${refs.baseIds.join("\u3001")}`);
1914
+ if (refs.selfAvatarId) lines.push(`- \u53D1\u6307\u4EE4\u7684\u4EBA\u81EA\u5DF1\u7684\u5934\u50CF\uFF1A${refs.selfAvatarId}\uFF08\u4ED6\u60F3\u753B\u300C\u6211\u300D\u65F6\u7528\u8FD9\u4E2A\uFF0C\u6216\u7528 get_avatar \u53D6\uFF09`);
1915
+ if (refs.atAvatarIds.length) {
1916
+ const who = refs.atTargets.map((t) => `${t.name || t.id}`).join("\u3001");
1917
+ lines.push(`- \u88AB @ \u7684\u4EBA\u7684\u5934\u50CF\uFF08\u5DF2\u81EA\u52A8\u5E26\u4E0A\uFF0C\u4E0D\u7528\u518D\u586B\uFF09\uFF1A${refs.atAvatarIds.join("\u3001")}\uFF08\u5206\u522B\u662F ${who} \u7684\u5934\u50CF\uFF09`);
1918
+ } else if (refs.atTargets.length) {
1919
+ lines.push(`- \u7528\u6237 @ \u4E86 ${refs.atTargets.map((t) => t.name || t.id).join("\u3001")}\uFF0C\u4F46\u6CA1\u53D6\u5230\u4ED6\u4EEC\u7684\u5934\u50CF\uFF0C\u9700\u8981\u7684\u8BDD\u7528 get_avatar \u518D\u8BD5`);
1920
+ }
1723
1921
  if (cmdConfig.custom) lines.push("- \u8FD9\u662F\u81EA\u5B9A\u4E49\u6307\u4EE4\uFF1A\u7528\u6237\u6CA1\u8BF4\u6E05\u695A\u8981\u753B\u4EC0\u4E48\u65F6\uFF0C\u5148\u7528 ask_user \u95EE\u4E00\u53E5");
1724
1922
  return instructions + lines.join("\n");
1725
1923
  }
@@ -1870,6 +2068,19 @@ ${echo}` : echo);
1870
2068
  promptText = mode === "rewrite" && await optimizePromptText("", text) || text;
1871
2069
  }
1872
2070
  const images = [...defaultImageUrls || [], ...extractImagesFromSession(session)];
2071
+ const avatarCfg = config.avatar || {};
2072
+ if (avatarCfg.enabled !== false) {
2073
+ if (avatarCfg.autoAt !== false) {
2074
+ for (const target of collectAtTargets(session)) {
2075
+ const url = await fetchAvatarUrl(session, target.id);
2076
+ if (url) images.push(url);
2077
+ }
2078
+ }
2079
+ if (avatarCfg.autoSelf && !images.length) {
2080
+ const url = await fetchAvatarUrl(session, session.userId);
2081
+ if (url) images.push(url);
2082
+ }
2083
+ }
1873
2084
  const remaining = Math.max(0, maxImages - images.length);
1874
2085
  if (remaining > 0) {
1875
2086
  const [msgId] = await session.send(
@@ -2288,6 +2499,33 @@ ${lines.join("\n")}`]);
2288
2499
  if (!content) return "";
2289
2500
  return import_koishi.h.select(content, "text").map((el) => el.attrs.content || "").join(" ").trim();
2290
2501
  }
2502
+ function collectAtTargets(session) {
2503
+ const raw = String(session?.content || session.stripped?.content || "");
2504
+ return extractAtIds(raw, session.selfId);
2505
+ }
2506
+ async function fetchAvatarUrl(session, userId) {
2507
+ if (!userId) return "";
2508
+ const cfg = config.avatar || {};
2509
+ const own = userId === session.userId;
2510
+ const url = buildAvatarUrl({
2511
+ userId,
2512
+ platform: session.platform,
2513
+ selfId: session.selfId,
2514
+ appId: session?.bot?.config?.id,
2515
+ authorAvatar: own ? String(session?.author?.avatar || "") : "",
2516
+ template: cfg.urlTemplate,
2517
+ size: Number(cfg.size) > 0 ? Number(cfg.size) : 640
2518
+ });
2519
+ if (url) return url;
2520
+ try {
2521
+ const member = session.guildId ? await session.bot?.getGuildMember?.(session.guildId, userId) : null;
2522
+ const got = member?.avatar || member?.user?.avatar;
2523
+ if (got) return String(got);
2524
+ } catch (error) {
2525
+ logInfo2(`\u5411\u9002\u914D\u5668\u7D22\u53D6\u5934\u50CF\u5931\u8D25: ${error}`);
2526
+ }
2527
+ return "";
2528
+ }
2291
2529
  function extractImagesFromSession(session) {
2292
2530
  const images = [];
2293
2531
  const currentImages = extractImagesFromMessage(session.stripped.content);
@@ -2453,12 +2691,14 @@ ${lines.join("\n")}`]);
2453
2691
  Config,
2454
2692
  apply,
2455
2693
  buildAgentTools,
2694
+ buildAvatarUrl,
2456
2695
  buildMarkdownImage,
2457
2696
  buildPromptBlock,
2458
2697
  buildPromptEcho,
2459
2698
  cleanAskMessage,
2460
2699
  computeRetryDelay,
2461
2700
  describeSelectorError,
2701
+ extractAtIds,
2462
2702
  extractJsonObject,
2463
2703
  extractResponseText,
2464
2704
  extractServerMessage,
@@ -2470,6 +2710,7 @@ ${lines.join("\n")}`]);
2470
2710
  mergePrompt,
2471
2711
  name,
2472
2712
  parseJsonToolCall,
2713
+ parsePolishIntent,
2473
2714
  parseRetryAfterHeader,
2474
2715
  pickAgentMessage,
2475
2716
  previewJson,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-image-prompt",
3
3
  "description": "多功能 AI 图像处理插件:Agent 模式把「查不查图库 / 要不要追问 / 什么时候开画」全交给模型自己决定,支持手办化、Coser 化、Minecraft 化等 47 种风格与自定义提示词",
4
- "version": "2.0.4",
4
+ "version": "2.1.0",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
package/readme.md CHANGED
@@ -7,9 +7,10 @@
7
7
  基于 OpenAI 兼容的 Chat Completions 接口,支持多种 AI 绘图模型,让你的图片瞬间变成精美手办。
8
8
 
9
9
  **特色能力**:**Agent 模式**(把「查不查图库 / 要不要追问 / 什么时候开画」全交给模型,像 NeoBot 一样多轮对话)·
10
- 参考图片分组 + 关键词检索 · 用户附加需求融合进提示词 · 提示词回显(QQ 走 markdown 代码块)·
11
- **指令一触发就回「收到」** · 提示词不再过度加工(绘图模型听得懂中文)· 生成结果自动入库形成闭环 ·
12
- 后台绘图 + 完成推送 · 参考图描述 AI 自动生成 · 429/限流自动退避重试。
10
+ **头像参考图**(@ 谁就用谁的头像画,也能画「我」)· **用户发的图直接进参考图** ·
11
+ 参考图片分组 + 关键词检索 · **默认不润色**(原话直出,用户点头才加工)·
12
+ 提示词回显(QQ 走 markdown 代码块)· **指令一触发就回「收到」** ·
13
+ 生成结果自动入库形成闭环 · 后台绘图 + 完成推送 · 参考图描述 AI 自动生成 · 429/限流自动退避重试。
13
14
 
14
15
  ## ⚙️ 插件设置
15
16
 
@@ -49,9 +50,10 @@
49
50
  ↓
50
51
  「收到,正在准备...」 ← 立刻回,不让用户干等
51
52
  ↓
52
- 模型拿到 3 个工具,自己决定怎么走:
53
+ 模型拿到 4 个工具,自己决定怎么走:
53
54
  ├─ gallery_search(关键词) → 自己想关键词、自己搜图库
54
55
  ├─ ask_user(问题) → 自己开口问、自己等用户回话
56
+ ├─ get_avatar(用户id) → 取群友头像当参考图
55
57
  └─ draw(prompt, references) → 自己写提示词、自己挑参考图、开画
56
58
  ↓
57
59
  模型说最后一句话 → 发给用户
@@ -66,6 +68,7 @@
66
68
  |---|---|
67
69
  | `gallery_search` | 按关键词搜参考图库(关键词由模型自己拟,空格分隔多词)。返回每条的 `id`,写进 `draw` 的 `references` |
68
70
  | `ask_user` | 把一句话发给用户**并等他的下一条消息**(他发的图片也会一起带回来,会拿到新编号) |
71
+ | `get_avatar` | 取某个人的头像当参考图。`userId` 留空 = 取发指令的人自己;也可以填被 @ 的人的 id |
69
72
  | `draw` | 真正开画。传最终提示词 + 参考图编号,画好自动发到群里 |
70
73
 
71
74
  ### 工作规则(写进提示词,由模型执行)
@@ -73,9 +76,11 @@
73
76
  1. **涉及角色时先搜图库**:有立绘就填进 `references`;没有就如实说「图库里没有这个立绘,将按描述创作」,然后正常画。
74
77
  2. 用户明说「不用参考 / 随意画 / 自由发挥」→ 跳过搜索,一次都不搜。
75
78
  3. **信息不够才开口**,而且一次只问一个最关键的问题(不连珠炮、不顺带聊别的)。
76
- 4. **开画前先问一句**(默认开):把「这次参考哪几张图 + 大致画成什么样」告诉你,你点头它才画。
79
+ 4. **开画前先问一句**(默认开):把「这次参考哪几张图 + 大致画成什么样」告诉你,并顺带问「提示词要不要我帮你润色」,你点头它才画。
77
80
  5. 画面上要出文字(台词/标题/招牌)→ 原样写进提示词,并说明「必须原样出现,不能变形或自创字形」。
78
81
  6. 只填它确认存在的编号,**不许编造 id**(编了会被丢掉并告诉它不存在)。
82
+ 7. **没指定参考图时先问一句**:用户这次没发图、没 @ 人、指令本身也没默认参考图时,不直接开画,先问要不要参考图。
83
+ 8. **默认不润色**:你的原话原封不动交给绘图模型。只有你说「帮我润色 / 优化一下」它才加工。
79
84
 
80
85
  ### Agent 配置
81
86
 
@@ -91,6 +96,8 @@
91
96
  | 采样温度 | — | 0.3 |
92
97
  | 输出长度上限 | **推理模型会把额度耗在思考上**,太小会一个字都生成不出来。0 = 不限制 | 8000 |
93
98
  | 记住最近几轮对话 | 同一频道记住最近 N 轮,「再画一张」「换个风格」才能接上;0 = 不记忆(重启即失效) | 6 |
99
+ | **没指定参考图时先问一句** | 用户没发图、没 @ 人、指令也没默认图时,开画前先问「要不要参考图」。插件兜底执行,模型自己先问过就不再问 | 开 |
100
+ | **开画前先问要不要润色** | 先问「提示词要不要我帮你润色」,**默认直接用你的原话**,你说润色才加工。同样是插件兜底 | 开 |
94
101
  | 工具调用日志 | 把每一轮的模型输出与工具调用写进日志,排查用 | 关 |
95
102
  | agent 专用模型 / 地址 / 密钥 | 留空则复用「AI 模型接口」里的配置 | 留空 |
96
103
  | 指令模板 | 只写「有哪些工具 + 什么时候用」,别写死流程。占位符 `{confirmRule}` 等 | 内置模板 |
@@ -103,6 +110,36 @@
103
110
  (带 tool 支持的版本)、`glm-4` 系等。老的小模型(如 `Qwen2.5-7B-Instruct`)可能不支持工具调用,
104
111
  这时就走 JSON 兜底协议,效果取决于模型听不听话。
105
112
 
113
+ ### 🧑 头像参考图(@ 谁画谁 / 画「我」)
114
+
115
+ 除了图库里的立绘,**群友的头像也能直接当参考图**。三种用法:
116
+
117
+ | 场景 | 怎么写 | 效果 |
118
+ |---|---|---|
119
+ | 画某个群友 | `画图/手办化 @小明 和我一起吃火锅` | 小明的头像**自动**作为参考图带上 |
120
+ | 画自己 | `画图/手办化 画我吃火锅` | 模型会调 `get_avatar` 取你自己的头像 |
121
+ | 追问里补图 | 模型问「要不要参考图」时直接**发一张图**、或 `@` 一个人、或说「用我的头像」 | 图/头像登记进参考图,下一轮就能用 |
122
+
123
+ **用户随消息发的图**(包括指令里直接带的、以及引用消息里的)**一律自动进参考图**,
124
+ 不用指望模型记得把它填进 `references` —— 之前这一步是模型自选,经常会漏掉。
125
+
126
+ | 配置项 | 说明 | 默认值 |
127
+ |---|---|---|
128
+ | 开启头像参考图 | 总开关 | 开 |
129
+ | @ 谁自动带谁的头像 | `@` 本身就是明确的指定,关掉就只能靠模型调 `get_avatar` | 开 |
130
+ | 没图时自动用自己的头像 | 开了的话,你没发图也没 @ 人时会自动带上自己的头像 | **关** |
131
+ | 头像边长 | 像素 | 640 |
132
+ | 自定义头像地址模板 | 留空用内置规则。占位符 `{userId}` `{platform}` `{selfId}` `{appId}` `{size}` | 空 |
133
+
134
+ **内置规则**(按平台走):
135
+
136
+ - **QQ 官方机器人**:`https://q.qlogo.cn/qqapp/{appid}/{openid}/{size}`(appid 从机器人配置里取)
137
+ - **onebot / red 这类拿得到真 QQ 号**:`https://q1.qlogo.cn/g?b=qq&nk={QQ号}&s={size}`
138
+ - 其它平台(Discord / KOOK 等):内置规则拼不出来,会去问适配器要成员头像;再拿不到就放弃(不影响正常出图)
139
+
140
+ > 平台不提供头像地址时,日志里不会有报错,只是这次没有头像参考图。
141
+ > 想换头像服务就填「自定义头像地址模板」。
142
+
106
143
  ### 🖼️ 参考图片组 / 图库
107
144
 
108
145
  参考图以**分组**的方式注册,agent 通过 `gallery_search` 按需搜。
@@ -156,9 +193,23 @@ bot:画好啦,按 ref1 的立绘做的~
156
193
  ```
157
194
 
158
195
  - 你回「算了」/ 不回 → 模型自己决定这次不画,**不会白画一张**。
159
- - 你直接发图 → 图会被登记成 `user1`、`user2`…,模型可以照样填进 `references`。
196
+ - 你直接发图 → 图会被登记成编号并**自动**作为参考图带上,不用模型再填。
197
+ - 你 `@` 一个人 → 那个人的头像自动作为参考图带上。
160
198
  - 「收到」那条不会重复发,提示词也只回显一次。
161
199
 
200
+ 再来一个用头像的例子:
201
+
202
+ ```
203
+ 你:表情包 @小明 我俩一起吃瓜
204
+
205
+ bot:收到,正在准备...
206
+ bot:这次用小明的头像当参考,画成表情包可以吗?提示词要我帮你润色吗(默认直接用你的原话)?
207
+ 你:直接画
208
+ bot:这两个人一起吃瓜
209
+ bot:![#400px #225px](https://...结果图)
210
+ bot:画好啦~
211
+ ```
212
+
162
213
  ### 📝 提示词回显
163
214
 
164
215
  处理时会把**最终交给绘图模型的提示词**一起发出来,方便你确认它到底用了什么。
@@ -239,6 +290,9 @@ bot:画好啦,按 ref1 的立绘做的~
239
290
  > **现在的绘图模型听得懂中文,也有基本的理解力。** 把用户那句话原样交给它就够了 ——
240
291
  > 反过来,把它「优化」成一两百字的英文术语堆砌,各要素互相稀释,出图反而更容易走样。
241
292
  > 所以 2.0.3 起默认是**做减法**。
293
+ >
294
+ > 2.1.0 起连「要不要润色」也交给你决定:**默认原封不动**,
295
+ > 只有你明确说「帮我润色 / 优化一下」模型才加工(agent 模式会在开画前问你这一句)。
242
296
 
243
297
  | 配置项 | 说明 | 默认 |
244
298
  |---|---|---|
package/src/index.ts CHANGED
@@ -212,7 +212,7 @@ export function resolveReferences(
212
212
  }
213
213
 
214
214
  /** agent 可用的工具名(JSON 兜底协议只对这几个名字生效,避免把正常回复当工具调用) */
215
- export const AGENT_TOOL_NAMES = ['gallery_search', 'ask_user', 'draw']
215
+ export const AGENT_TOOL_NAMES = ['gallery_search', 'ask_user', 'get_avatar', 'draw']
216
216
 
217
217
  /** OpenAI function-calling 格式的工具定义 */
218
218
  export function buildAgentTools(): any[] {
@@ -246,6 +246,20 @@ export function buildAgentTools(): any[] {
246
246
  },
247
247
  },
248
248
  },
249
+ {
250
+ type: 'function',
251
+ function: {
252
+ name: 'get_avatar',
253
+ description: '取某个人的头像当参考图,返回它的编号。userId 留空 = 取发指令的人自己的头像;也可以填「被 @ 的人」列表里的 id。用户说「画我」「用我的头像」「画 @某某」时用它。',
254
+ parameters: {
255
+ type: 'object',
256
+ properties: {
257
+ userId: { type: 'string', description: '要取头像的用户 id(取自上下文里的「被 @ 的人」),留空或写 me/self 表示发指令的人自己' },
258
+ },
259
+ required: [],
260
+ },
261
+ },
262
+ },
249
263
  {
250
264
  type: 'function',
251
265
  function: {
@@ -254,8 +268,8 @@ export function buildAgentTools(): any[] {
254
268
  parameters: {
255
269
  type: 'object',
256
270
  properties: {
257
- // 提示词要短:宁可朴素,也不要堆术语写成几百字的「专业」prompt
258
- prompt: { type: 'string', description: '绘图提示词。说人话、写简短就行,中文即可(绘图模型听得懂中文);不要把用户的话扩写成几百字,也不要堆砌风格/材质/光线术语。画面上必须出现的文字要原样写进去' },
271
+ // 提示词要短;用户没说要润色就原封不动用他的原话
272
+ prompt: { type: 'string', description: '绘图提示词。**默认原封不动用用户的话**:简短、说人话,不要扩写成几百字,不要补画风/光线/材质术语,不要翻成英文;只有用户说过「帮我润色 / 优化一下」才加工。中文即可(绘图模型听得懂中文)。画面上必须出现的文字要原样写进去' },
259
273
  references: {
260
274
  type: 'array',
261
275
  items: { type: 'string' },
@@ -369,6 +383,78 @@ export function cleanAskMessage(raw: any, max = 200): string {
369
383
  return text
370
384
  }
371
385
 
386
+ /**
387
+ * 从用户的回答里判断他要不要润色提示词。
388
+ * 默认 false —— 用户的原话原封不动交给绘图模型才是对的默认值。
389
+ */
390
+ export function parsePolishIntent(text: string): boolean {
391
+ const t = String(text || '')
392
+ if (!t) return false
393
+ if (/不(用|要|必)?(润色|优化|加工|改)|别(润色|优化|加工)|原样|原封不动|直接画|不用了/.test(t)) return false
394
+ return /润色|优化一下|加工|丰富|扩写|细化|详细[一点些]|写(得)?(更)?(完整|详细)|帮我(写|改)/.test(t)
395
+ }
396
+
397
+ /** 从消息内容里解析 @ 到的人。type="all"(@全体成员)和 @机器人自己 会被跳过 */
398
+ export function extractAtIds(content: string, selfId?: string): { id: string, name: string }[] {
399
+ if (!content) return []
400
+ const out: { id: string, name: string }[] = []
401
+ const seen = new Set<string>()
402
+ for (const el of h.select(content, 'at')) {
403
+ const id = String(el.attrs.id ?? el.attrs.user ?? '').trim()
404
+ if (!id || id === selfId) continue
405
+ if (String(el.attrs.type || '') === 'all') continue
406
+ if (seen.has(id)) continue
407
+ seen.add(id)
408
+ out.push({ id, name: String(el.attrs.name || el.attrs.nick || '').trim() })
409
+ }
410
+ return out
411
+ }
412
+
413
+ /**
414
+ * 拼头像地址。优先级:自定义模板 > 适配器直接给的 > 平台内置规则。
415
+ * 拼不出来返回空字符串(调用方再决定是问适配器还是放弃)。
416
+ */
417
+ export function buildAvatarUrl(input: {
418
+ userId: string
419
+ platform?: string
420
+ selfId?: string
421
+ appId?: string
422
+ /** 发送者自己的头像(适配器已经在消息里带过来了) */
423
+ authorAvatar?: string
424
+ template?: string
425
+ size?: number
426
+ }): string {
427
+ const userId = String(input?.userId || '').trim()
428
+ if (!userId) return ''
429
+
430
+ const size = Number(input.size) > 0 ? Number(input.size) : 640
431
+ const platform = String(input.platform || '')
432
+ const appId = String(input.appId || '')
433
+
434
+ if (input.template) {
435
+ return String(input.template).replace(/\{(\w+)\}/g, (_m, key) => {
436
+ const vars: Record<string, string> = {
437
+ userId, platform, size: String(size),
438
+ selfId: String(input.selfId || ''),
439
+ appId,
440
+ }
441
+ return vars[key] ?? ''
442
+ })
443
+ }
444
+
445
+ if (input.authorAvatar) return String(input.authorAvatar)
446
+
447
+ // QQ 官方机器人:https://q.qlogo.cn/qqapp/{appid}/{openid}/{size}
448
+ if (appId && (platform === 'qq' || platform === 'qqguild')) {
449
+ return `https://q.qlogo.cn/qqapp/${appId}/${userId}/${size}`
450
+ }
451
+ // 拿得到真 QQ 号时走通用头像服务(onebot / red 等)
452
+ if (/^(qq|onebot|red|qqguild)$/.test(platform) && /^\d{5,12}$/.test(userId)) {
453
+ return `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=${size}`
454
+ }
455
+ return ''
456
+ }
457
+
372
458
  /**
373
459
  * 图库容量裁剪:超出容量时淘汰最旧的(按 time 升序),只保留每个组最近 capacity 条
374
460
  */
@@ -880,6 +966,7 @@ interface CommandConfig {
880
966
  referenceGroups?: ReferenceGroup[]
881
967
  aiSelector?: AISelectorConfig
882
968
  agent?: AgentConfig
969
+ avatar?: AvatarConfig
883
970
  showPrompt?: boolean
884
971
  promptMaxLength?: number
885
972
  appendUserInput?: boolean
@@ -999,6 +1086,24 @@ interface AgentConfig {
999
1086
  historyTurns: number
1000
1087
  /** 把每一轮的工具调用打到日志里,方便排查 */
1001
1088
  debugLog: boolean
1089
+ /** 用户没给任何参考图时,开画前先问一句「要不要参考图」 */
1090
+ askIfNoReference: boolean
1091
+ /** 开画前先问一句「要不要润色提示词」(默认不润色,原话直出) */
1092
+ askBeforePolish: boolean
1093
+ }
1094
+
1095
+ /** 头像参考图配置 */
1096
+ interface AvatarConfig {
1097
+ /** 总开关:能不能拿用户头像当参考图 */
1098
+ enabled: boolean
1099
+ /** 用户 @ 了谁,就自动把那个人的头像作为参考图(@ 本身就是明确指定) */
1100
+ autoAt: boolean
1101
+ /** 用户没发图也没 @ 人时,自动拿他自己的头像当参考图 */
1102
+ autoSelf: boolean
1103
+ /** 头像尺寸(边长像素) */
1104
+ size: number
1105
+ /** 自定义头像地址模板,留空用内置规则。占位符:{userId} {platform} {selfId} {appId} {size} */
1106
+ urlTemplate: string
1002
1107
  }
1003
1108
 
1004
1109
  /** agent 参考图登记表里的一条 */
@@ -1007,7 +1112,7 @@ export interface RefEntry {
1007
1112
  url: string
1008
1113
  description: string
1009
1114
  group: string
1010
- source: 'gallery' | 'user' | 'cmd'
1115
+ source: 'gallery' | 'user' | 'cmd' | 'avatar'
1011
1116
  }
1012
1117
 
1013
1118
  /** 交给 AI 挑选的候选图片 */
@@ -1056,6 +1161,7 @@ const DEFAULT_AGENT_INSTRUCTIONS = `你是 QQ 群里的绘图助手。用户发
1056
1161
  - gallery_search:按关键词搜索参考图库。**涉及具体角色/立绘时必须先搜一次**。
1057
1162
  多个关键词用空格分隔(如「白发 立绘」),全部命中的排在最前;搜不到就换个词再搜。
1058
1163
  - ask_user:把问题发给用户**并等他回复**(他发的图也会一起带回来)。
1164
+ - get_avatar:取某个人的头像当参考图。userId 留空取发指令者自己;也可以取被 @ 的人的头像。
1059
1165
  - draw:真正开始画。把 prompt 和挑中的参考图编号交进去,画好会自动发到群里。
1060
1166
 
1061
1167
  工作规则:
@@ -1070,22 +1176,35 @@ const DEFAULT_AGENT_INSTRUCTIONS = `你是 QQ 群里的绘图助手。用户发
1070
1176
  4. 画面上要出现文字(台词、标题、招牌、字幕)时,把文字原样写进 prompt,并说明「这些字必须原样出现,不能变形或自创字形」。
1071
1177
  5. draw 返回 ok=false 时,看 error 决定:能改的(提示词问题)就换个写法重试,改不了的(限流/配额)就如实告诉用户。
1072
1178
  6. 流程结束后用中文回一句简短的结果说明(不要复述 prompt,不要写小作文,不要再问「还要我做什么」)。
1073
- 7. 图片编号:gallery_search 结果里的 id、用户发的图(user1、user2…)、指令默认图(cmd1…)都可以直接写进 draw 的 references。
1074
- 只填你确认存在的编号,**绝对不要编造 id**。
1075
- 8. 【prompt 要短,要说人话】**不要**把用户的话扩写成几百字的「专业」提示词:
1179
+ 7. 图片编号:gallery_search 结果里的 id、用户随消息发的图、头像、指令默认图都可以直接写进 draw 的 references。
1180
+ 只填上下文里**明确列出过**的编号,**绝对不要编造 id**。
1181
+ - 用户随消息发的图会自动带上,不用你再填;
1182
+ - 他 @ 了谁,那个人的头像也会自动带上。
1183
+ 8. 【没指定参考图时先问一句】用户这次没发图、没 @ 人、指令本身也没有默认参考图,你也搜不到合适的立绘时,
1184
+ **不要直接 draw**。先用 ask_user 问一句要不要参考图,并给出选项(例如「用你自己的头像 / 用 @某某 的头像 /
1185
+ 我帮你从图库找一张 / 不用参考直接画」)。用户说「不用 / 随便 / 直接画」就立刻开画,**不要问第二次**。
1186
+ 9. 【要画「我」就用头像】用户说「画我」「用我的形象」「画我和 xxx」却没给图时,用 get_avatar 取他自己的头像;
1187
+ 说「画 @某某」时取那个人的头像(一般已经自动带上了,直接用那个编号即可)。
1188
+ 10. 【默认不润色,用户点头才润色】把用户的话**原封不动**交给绘图模型就是最好的默认值:
1189
+ - **不要**主动扩写、不要补画风/光线/材质/镜头、不要翻成英文、不要加「masterpiece」这类词;
1190
+ - 用户明确说「帮我润色 / 优化一下 / 写详细点」时,你才把提示词写得更完整;
1191
+ - draw 工具返回 asked=true 时看它给的 polish 字段:polish=true 才润色,false 或没给就**一个字都别改**;
1076
1192
  - 用户用中文提的需求,prompt 就写中文(绘图模型听得懂中文,理解力也够用);
1077
- - 别为了显得专业就去堆风格 / 材质 / 光线 / 镜头术语,更别刻意翻成英文;
1078
1193
  - 只保留用户真正说过的内容 + 画面必须有的要素,**不要替他脑补细节**;
1079
1194
  - 提示词越长,各要素互相稀释,出图反而越走样。一两句话能把画面说清就够了。
1080
1195
 
1081
1196
  如果你所在的接口不支持函数调用,就**只输出一行 JSON** 来调用工具,例如:
1082
1197
  {"tool":"gallery_search","args":{"keyword":"白发 立绘"}}
1083
1198
  {"tool":"ask_user","args":{"question":"想要什么画风?"}}
1199
+ {"tool":"get_avatar","args":{"userId":""}}
1084
1200
  {"tool":"draw","args":{"prompt":"...","references":["ref1","user1"]}}
1085
1201
  不想调用工具时,正常输出给用户看的中文即可。`
1086
1202
 
1087
1203
  /** 「开画前先问一句」的两种提示词分支 */
1088
- const AGENT_CONFIRM_RULE_ON = `**必须先 ask_user**:用一句话告诉用户「这次会参考哪几张图 + 大致画成什么样」,用户点头后才能调用 draw。
1204
+ const AGENT_CONFIRM_RULE_ON = `**必须先 ask_user**:用一句话告诉用户「这次会参考哪几张图 + 大致画成什么样」,
1205
+ 并顺带问一句「提示词要不要我帮你润色(默认直接用你的原话)」,用户点头后才能调用 draw。
1206
+ - 用户说「润色 / 优化 / 丰富一下」→ 你再把提示词写得更完整;
1207
+ - 用户说「直接画 / 不用 / 原样」或没提这件事 → **原封不动用他的原话**,一个字都别润色。
1089
1208
  用户回复了否定或提出修改意见,就按他说的改,再确认一次或直接画(他说「直接画」就不用再问)。`
1090
1209
  const AGENT_CONFIRM_RULE_OFF = `用户需求已经明确的,直接调用 draw,不用多问一句。只有真的缺关键信息才 ask_user。`
1091
1210
 
@@ -1093,6 +1212,7 @@ const AGENT_CONFIRM_RULE_OFF = `用户需求已经明确的,直接调用 draw
1093
1212
  const AGENT_JSON_FALLBACK_RULE = `(注意:当前接口不支持函数调用)请你**只用一行 JSON** 来调用工具,不要输出别的内容:
1094
1213
  {"tool":"gallery_search","args":{"keyword":"白发 立绘"}}
1095
1214
  {"tool":"ask_user","args":{"question":"想要什么画风?"}}
1215
+ {"tool":"get_avatar","args":{"userId":""}}
1096
1216
  {"tool":"draw","args":{"prompt":"...","references":["ref1","ref2"]}}
1097
1217
  不需要调用工具时,正常输出给用户看的中文即可。`
1098
1218
 
@@ -1648,9 +1768,21 @@ export const Config: Schema = Schema.intersect([
1648
1768
  baseUrl: Schema.string().role('link').description('agent 专用接口地址(留空则复用「AI 模型接口」的地址)'),
1649
1769
  apiKey: Schema.string().role('secret').description('agent 专用密钥(留空则复用「AI 模型接口」的密钥)'),
1650
1770
  instructions: Schema.string().role('textarea', { rows: [16, 8] }).default(DEFAULT_AGENT_INSTRUCTIONS).description('驱动 agent 行为的系统提示词(只写「有哪些工具 + 什么时候用」,别写死流程)。可用占位符:{confirmRule} 开画确认规则、{command} 指令名、{prompt} 指令提示词、{userInput} 用户附加需求、{imageCount} 用户随消息发的图数量、{refCount} 可检索的参考图数量。指令名/提示词等上下文会自动附在提示词末尾,不用自己引用'),
1771
+ askIfNoReference: Schema.boolean().default(true).description('用户这次既没发图、也没 @ 人、指令本身也没有默认参考图时,开画前先问一句「要不要参考图」(可以用他自己的头像 / @ 别人的头像)。这条是插件兜底执行的,模型自己先问过就不再问'),
1772
+ askBeforePolish: Schema.boolean().default(true).description('开画前先问用户「提示词要不要我帮你润色」。**默认是原封不动把用户的原话交给绘图模型**,只有用户说「润色 / 优化」才加工。这条同样是插件兜底,模型自己先问过就不再问'),
1651
1773
  }).collapse().description('Agent 配置(指令较长,已折叠)'),
1652
1774
  }).description('Agent 模式'),
1653
1775
 
1776
+ Schema.object({
1777
+ avatar: Schema.object({
1778
+ enabled: Schema.boolean().default(true).description('开启头像参考图:可以把群友的头像当参考图来画'),
1779
+ autoAt: Schema.boolean().default(true).description('用户 @ 了谁,就自动把那个人的头像作为参考图(@ 本身就是明确的指定)'),
1780
+ autoSelf: Schema.boolean().default(false).description('用户没发图也没 @ 人时,自动拿他自己的头像当参考图。关掉也没关系,「用我的头像画」这类需求 agent 会自己调用 get_avatar'),
1781
+ size: Schema.number().default(640).min(64).max(1024).step(64).description('头像边长(像素)'),
1782
+ urlTemplate: Schema.string().default('').description('自定义头像地址模板,留空用内置规则。占位符:{userId} 用户 id、{platform} 平台、{selfId} 机器人 id、{appId} 机器人 appid、{size} 尺寸'),
1783
+ }).description('头像参考图'),
1784
+ }).description('头像参考图'),
1785
+
1654
1786
  Schema.object({
1655
1787
  appendUserInput: Schema.boolean().default(true).description('把用户随指令发的附加需求并入绘图提示词(例如「手办化 xxx 在偷吃白饭被发现的表情」)'),
1656
1788
  promptOptimize: Schema.union([
@@ -1890,6 +2022,8 @@ export function apply(ctx: Context, config: CommandConfig) {
1890
2022
  needimages: '请提供至少一张图片',
1891
2023
  agentfailed: '绘图助手这次没跑起来({0})。可以先关掉「Agent 模式」照常画图,或换一个支持工具调用的模型。',
1892
2024
  agentnoresult: '这次没能理出个结果来,麻烦再说一次你想画什么~',
2025
+ askreference: '这次没有指定参考图。要参考图片吗?可以直接发图、@ 一个人用他的头像,或者说「用我的头像」;不需要就回「直接画」。\n另外,提示词要我帮你润色一下吗?默认直接用你的原话。',
2026
+ askpolish: '这次直接按你的原话开画。要我先帮你润色一下提示词吗?回「润色」我加工一下,回「直接画」就原样交给绘图模型。',
1893
2027
  optimizefailed: '(提示词融合失败,已改为追加模式)',
1894
2028
  bgstart: '已开始后台绘图({0} 张图),画好后会在这里通知你...',
1895
2029
  bgqueue: '(当前有 {0} 个任务在排队)',
@@ -2002,7 +2136,31 @@ export function apply(ctx: Context, config: CommandConfig) {
2002
2136
  const baseIds = (defaultImageUrls || []).filter(Boolean).map(url => register('cmd', url, '', '指令默认图'))
2003
2137
  const userIds = extractImagesFromSession(session).map(url => register('user', url, '', '用户发送'))
2004
2138
 
2005
- logInfo(`[${cmdConfig.name}] agent 启动:可搜索参考图 ${pool.length} 张,默认图 ${baseIds.length} 张,用户随消息发的图 ${userIds.length} 张`)
2139
+ // ---- 头像参考图 ----
2140
+ // 自己的头像登记进表(模型要画「我」时可以取用);@ 到的人的头像直接作为参考图带上
2141
+ const avatarCfg: AvatarConfig = config.avatar || ({} as AvatarConfig)
2142
+ const avatarOn = avatarCfg.enabled !== false
2143
+ const atTargets = avatarOn ? collectAtTargets(session) : []
2144
+ let selfAvatarId = ''
2145
+ const atAvatarIds: string[] = []
2146
+ const atAvatarById = new Map<string, string>()
2147
+ if (avatarOn) {
2148
+ const selfUrl = await fetchAvatarUrl(session, session.userId)
2149
+ if (selfUrl) {
2150
+ selfAvatarId = register('avatar', selfUrl, '发指令的人的头像', '头像')
2151
+ }
2152
+ if (avatarCfg.autoAt !== false) {
2153
+ for (const target of atTargets) {
2154
+ const url = await fetchAvatarUrl(session, target.id)
2155
+ if (!url) continue
2156
+ const id = register('avatar', url, `${target.name || target.id} 的头像`, '头像')
2157
+ if (!atAvatarIds.includes(id)) atAvatarIds.push(id)
2158
+ atAvatarById.set(target.id, id)
2159
+ }
2160
+ }
2161
+ }
2162
+
2163
+ logInfo(`[${cmdConfig.name}] agent 启动:可搜索参考图 ${pool.length} 张,默认图 ${baseIds.length} 张,用户随消息发的图 ${userIds.length} 张,头像 ${atAvatarIds.length + (selfAvatarId ? 1 : 0)} 张`)
2006
2164
 
2007
2165
  // ---- 历史(同一频道记住最近几轮,让「再画一张」能接上)----
2008
2166
  const historyKey = agentHistoryKey(session)
@@ -2012,7 +2170,9 @@ export function apply(ctx: Context, config: CommandConfig) {
2012
2170
  remember({ role: 'user', content: userInputText || '(用户没有附加说明)' })
2013
2171
 
2014
2172
  const messages: any[] = [
2015
- { role: 'system', content: buildAgentSystemPrompt(cmdConfig, userInputText, userIds.length, pool.length, baseIds, agentCfg) },
2173
+ { role: 'system', content: buildAgentSystemPrompt(cmdConfig, userInputText, pool.length, {
2174
+ userIds, baseIds, atAvatarIds, selfAvatarId, atTargets,
2175
+ }, agentCfg) },
2016
2176
  ...history,
2017
2177
  { role: 'user', content: userInputText ? userInputText : '(用户没有附加说明,请按指令自带的提示词来)' },
2018
2178
  ]
@@ -2023,6 +2183,10 @@ export function apply(ctx: Context, config: CommandConfig) {
2023
2183
  let finalText = ''
2024
2184
  let drew = false
2025
2185
  let failed = ''
2186
+ /** 这轮里模型自己有没有问过用户(问过就不再替它追问参考图的事) */
2187
+ let askedOnce = false
2188
+ /** 开画前的兜底确认已经问过了,一次流程只问一次 */
2189
+ let drawAsked = false
2026
2190
 
2027
2191
  for (let iteration = 0; iteration < maxIterations; iteration++) {
2028
2192
  if (!isActive || !ctx.scope.isActive) break
@@ -2088,8 +2252,9 @@ export function apply(ctx: Context, config: CommandConfig) {
2088
2252
  switch (name) {
2089
2253
  case 'gallery_search': return toolGallerySearch(args)
2090
2254
  case 'ask_user': return toolAskUser(args)
2255
+ case 'get_avatar': return toolGetAvatar(args)
2091
2256
  case 'draw': return toolDraw(args)
2092
- default: return JSON.stringify({ ok: false, error: `没有叫「${name}」的工具,可用的是 gallery_search / ask_user / draw` })
2257
+ default: return JSON.stringify({ ok: false, error: `没有叫「${name}」的工具,可用的是 gallery_search / ask_user / get_avatar / draw` })
2093
2258
  }
2094
2259
  }
2095
2260
 
@@ -2114,10 +2279,35 @@ export function apply(ctx: Context, config: CommandConfig) {
2114
2279
  })
2115
2280
  }
2116
2281
 
2282
+ /** 取某人头像:留空取发指令的人,也可以取被 @ 的人 */
2283
+ async function toolGetAvatar(args: any): Promise<string> {
2284
+ if (!avatarOn) return JSON.stringify({ ok: false, error: '头像参考图功能已关闭' })
2285
+ const raw = String(args?.userId ?? args?.id ?? args?.user ?? '').trim()
2286
+ if (!raw || /^(me|self|自己|我)$/i.test(raw) || raw === session.userId) {
2287
+ if (selfAvatarId) {
2288
+ return JSON.stringify({ ok: true, id: selfAvatarId, userId: session.userId, note: '发指令的人自己的头像' })
2289
+ }
2290
+ const url = await fetchAvatarUrl(session, session.userId)
2291
+ if (!url) return JSON.stringify({ ok: false, error: '没取到他自己的头像(这个平台可能不提供头像地址)' })
2292
+ selfAvatarId = register('avatar', url, '发指令的人的头像', '头像')
2293
+ return JSON.stringify({ ok: true, id: selfAvatarId, userId: session.userId, note: '发指令的人自己的头像' })
2294
+ }
2295
+
2296
+ const cached = atAvatarById.get(raw)
2297
+ if (cached) return JSON.stringify({ ok: true, id: cached, userId: raw, note: '被 @ 的人的头像' })
2298
+
2299
+ const url = await fetchAvatarUrl(session, raw)
2300
+ if (!url) return JSON.stringify({ ok: false, error: `没取到 ${raw} 的头像` })
2301
+ const id = register('avatar', url, `${raw} 的头像`, '头像')
2302
+ atAvatarById.set(raw, id)
2303
+ return JSON.stringify({ ok: true, id, userId: raw, note: '这个人的头像' })
2304
+ }
2305
+
2117
2306
  /** 提问并等用户的下一条消息(图也会一起带回来) */
2118
2307
  async function toolAskUser(args: any): Promise<string> {
2119
2308
  const question = cleanAskMessage(args?.question ?? args?.text ?? args?.message)
2120
2309
  if (!question) return JSON.stringify({ ok: false, error: 'question 不能为空' })
2310
+ askedOnce = true
2121
2311
 
2122
2312
  try {
2123
2313
  await sendNotice(session, question)
@@ -2146,10 +2336,34 @@ export function apply(ctx: Context, config: CommandConfig) {
2146
2336
  remember({ role: 'user', content: text || (ids.length ? `(用户发了 ${ids.length} 张图片)` : '(用户什么都没说)') })
2147
2337
  logInfo(`agent 提问「${question}」-> 用户回复: ${text || '(无文字)'},图片 ${ids.length} 张`)
2148
2338
 
2339
+ // 回复里 @ 了谁 / 说「用我的头像」→ 把对应头像也登记进来
2340
+ const avatars: { id: string, note: string }[] = []
2341
+ if (avatarOn) {
2342
+ if (avatarCfg.autoAt !== false) {
2343
+ for (const target of extractAtIds(String(answer || ''), session.selfId)) {
2344
+ const cached = atAvatarById.get(target.id)
2345
+ if (cached) {
2346
+ if (!avatars.some(a => a.id === cached)) avatars.push({ id: cached, note: `${target.name || target.id} 的头像` })
2347
+ continue
2348
+ }
2349
+ const url = await fetchAvatarUrl(session, target.id)
2350
+ if (!url) continue
2351
+ const id = register('avatar', url, `${target.name || target.id} 的头像`, '头像')
2352
+ atAvatarById.set(target.id, id)
2353
+ avatars.push({ id, note: `${target.name || target.id} 的头像` })
2354
+ }
2355
+ }
2356
+ // 「用我的头像」「画我」——就算关闭了 autoSelf,用户明确开口了就给他
2357
+ if (/头像|我的形象|画我|我自己/.test(text) && selfAvatarId) {
2358
+ if (!avatars.some(a => a.id === selfAvatarId)) avatars.push({ id: selfAvatarId, note: '发指令的人自己的头像' })
2359
+ }
2360
+ }
2361
+
2149
2362
  return JSON.stringify({
2150
2363
  ok: true,
2151
2364
  text,
2152
2365
  images: ids.map((id, i) => ({ id, note: `用户这次发的第 ${i + 1} 张图` })),
2366
+ ...(avatars.length ? { avatars } : {}),
2153
2367
  })
2154
2368
  }
2155
2369
 
@@ -2168,10 +2382,41 @@ export function apply(ctx: Context, config: CommandConfig) {
2168
2382
  const wanted: string[] = []
2169
2383
  const pushId = (id: string) => { if (id && !wanted.includes(id)) wanted.push(id) }
2170
2384
  baseIds.forEach(pushId)
2385
+ // 用户随消息发的图、他 @ 的人的头像:都是他明确指定的,直接进参考图,不依赖模型记得填
2386
+ userIds.forEach(pushId)
2387
+ atAvatarIds.forEach(pushId)
2388
+ if (avatarOn && avatarCfg.autoSelf) pushId(selfAvatarId)
2171
2389
  ids.forEach(pushId)
2172
2390
  const capped = wanted.slice(0, maxSelect)
2173
2391
  const imageUrls = capped.map(id => registry.get(id)?.url).filter(Boolean) as string[]
2174
2392
 
2393
+ // 开画前兜底确认:一次流程最多问一次;模型自己已经问过就不再打扰
2394
+ // 两个话题合并进同一句话 —— 参考图要不要 + 提示词要不要润色
2395
+ const needRef = !capped.length && agentCfg.askIfNoReference !== false
2396
+ const needPolish = agentCfg.askBeforePolish !== false
2397
+ if (!drawAsked && !askedOnce && (needRef || needPolish)) {
2398
+ drawAsked = true
2399
+ const asked = await toolAskUser({
2400
+ question: needRef
2401
+ ? session.text('image-prompt.messages.askreference')
2402
+ : session.text('image-prompt.messages.askpolish'),
2403
+ })
2404
+ let parsed: any = {}
2405
+ try { parsed = JSON.parse(asked) } catch { parsed = {} }
2406
+ // 「不用润色 / 直接画 / 原样」→ false;明确说「润色 / 优化」→ true。默认不润色
2407
+ const polish = parsed.ok ? parsePolishIntent(parsed.text) : false
2408
+ return JSON.stringify({
2409
+ ok: false,
2410
+ asked: true,
2411
+ polish,
2412
+ ...(parsed.images?.length ? { newImages: parsed.images } : {}),
2413
+ ...(parsed.avatars?.length ? { newAvatars: parsed.avatars } : {}),
2414
+ note: polish
2415
+ ? '已经替你问过用户了:他说要润色,可以把提示词写得更完整。他给了图/头像也要写进 references。重新调用 draw 开画,不要再问第二次'
2416
+ : '已经替你问过用户了:提示词**原封不动用他的原话**,不要润色、不要扩写、不要翻译。他给了图/头像就写进 references。重新调用 draw 开画,不要再问第二次',
2417
+ })
2418
+ }
2419
+
2175
2420
  const finalPrompt = args?.negative_prompt
2176
2421
  ? `${prompt}\n\nNegative prompt: ${String(args.negative_prompt).trim()}`
2177
2422
  : prompt
@@ -2202,16 +2447,21 @@ export function apply(ctx: Context, config: CommandConfig) {
2202
2447
  function buildAgentSystemPrompt(
2203
2448
  cmdConfig: CommandConfig['nested']['commands'][number],
2204
2449
  userInputText: string,
2205
- imageCount: number,
2206
2450
  refCount: number,
2207
- baseIds: string[],
2451
+ refs: {
2452
+ userIds: string[]
2453
+ baseIds: string[]
2454
+ atAvatarIds: string[]
2455
+ selfAvatarId: string
2456
+ atTargets: { id: string, name: string }[]
2457
+ },
2208
2458
  agentCfg: AgentConfig
2209
2459
  ): string {
2210
2460
  const instructions = renderAgentInstructions(agentCfg.instructions || DEFAULT_AGENT_INSTRUCTIONS, {
2211
2461
  command: cmdConfig.name || '',
2212
2462
  prompt: cmdConfig.prompt || '',
2213
2463
  userInput: userInputText || '',
2214
- imageCount,
2464
+ imageCount: refs.userIds.length,
2215
2465
  refCount,
2216
2466
  confirmRule: agentCfg.confirmBeforeDraw !== false ? AGENT_CONFIRM_RULE_ON : AGENT_CONFIRM_RULE_OFF,
2217
2467
  })
@@ -2220,10 +2470,17 @@ export function apply(ctx: Context, config: CommandConfig) {
2220
2470
  `- 绘图指令:${cmdConfig.name}`,
2221
2471
  `- 指令自带的提示词:${cmdConfig.prompt || '(空,这是自定义指令)'}`,
2222
2472
  `- 用户这次说的话:${userInputText || '(用户没说话)'}`,
2223
- `- 用户随消息发的图:${imageCount} 张${imageCount ? '(编号见下方,可以直接写进 draw 的 references)' : ''}`,
2473
+ `- 用户随消息发的图:${refs.userIds.length ? `${refs.userIds.join('、')}(已自动带上,不用再填)` : '0 张'}`,
2224
2474
  `- 图库里可以搜到的参考图:${refCount} 张`,
2225
2475
  ]
2226
- if (baseIds.length) lines.push(`- 指令默认参考图(已自动带上,不用再填):${baseIds.join('、')}`)
2476
+ if (refs.baseIds.length) lines.push(`- 指令默认参考图(已自动带上,不用再填):${refs.baseIds.join('、')}`)
2477
+ if (refs.selfAvatarId) lines.push(`- 发指令的人自己的头像:${refs.selfAvatarId}(他想画「我」时用这个,或用 get_avatar 取)`)
2478
+ if (refs.atAvatarIds.length) {
2479
+ const who = refs.atTargets.map(t => `${t.name || t.id}`).join('、')
2480
+ lines.push(`- 被 @ 的人的头像(已自动带上,不用再填):${refs.atAvatarIds.join('、')}(分别是 ${who} 的头像)`)
2481
+ } else if (refs.atTargets.length) {
2482
+ lines.push(`- 用户 @ 了 ${refs.atTargets.map(t => t.name || t.id).join('、')},但没取到他们的头像,需要的话用 get_avatar 再试`)
2483
+ }
2227
2484
  if (cmdConfig.custom) lines.push('- 这是自定义指令:用户没说清楚要画什么时,先用 ask_user 问一句')
2228
2485
 
2229
2486
  return instructions + lines.join('\n')
@@ -2428,6 +2685,22 @@ export function apply(ctx: Context, config: CommandConfig) {
2428
2685
  }
2429
2686
 
2430
2687
  const images: string[] = [...(defaultImageUrls || []), ...extractImagesFromSession(session)]
2688
+
2689
+ // 头像参考图:@ 了谁就带上谁的头像;一张图都没有且开了 autoSelf 时带上自己的
2690
+ const avatarCfg: AvatarConfig = config.avatar || ({} as AvatarConfig)
2691
+ if (avatarCfg.enabled !== false) {
2692
+ if (avatarCfg.autoAt !== false) {
2693
+ for (const target of collectAtTargets(session)) {
2694
+ const url = await fetchAvatarUrl(session, target.id)
2695
+ if (url) images.push(url)
2696
+ }
2697
+ }
2698
+ if (avatarCfg.autoSelf && !images.length) {
2699
+ const url = await fetchAvatarUrl(session, session.userId)
2700
+ if (url) images.push(url)
2701
+ }
2702
+ }
2703
+
2431
2704
  const remaining = Math.max(0, maxImages - images.length)
2432
2705
  if (remaining > 0) {
2433
2706
  const [msgId] = await session.send(
@@ -2987,6 +3260,46 @@ export function apply(ctx: Context, config: CommandConfig) {
2987
3260
  .trim()
2988
3261
  }
2989
3262
 
3263
+ /**
3264
+ * 取这条消息里 @ 到的人。
3265
+ * 注意不能只看 stripped.content —— Koishi 的 stripped 会把开头连续的 <at> 元素剥掉,
3266
+ * 「/画图 @某某」这种写法里 @ 就丢了,必须看原始 content。
3267
+ */
3268
+ function collectAtTargets(session: Session): { id: string, name: string }[] {
3269
+ const raw = String((session as any)?.content || session.stripped?.content || '')
3270
+ return extractAtIds(raw, session.selfId)
3271
+ }
3272
+
3273
+ /** 取某个人的头像地址:先按平台规则拼,拼不出来再去问适配器要 */
3274
+ async function fetchAvatarUrl(session: Session, userId: string): Promise<string> {
3275
+ if (!userId) return ''
3276
+ const cfg: AvatarConfig = config.avatar || ({} as AvatarConfig)
3277
+ const own = userId === session.userId
3278
+
3279
+ const url = buildAvatarUrl({
3280
+ userId,
3281
+ platform: session.platform,
3282
+ selfId: session.selfId,
3283
+ appId: (session as any)?.bot?.config?.id,
3284
+ authorAvatar: own ? String((session as any)?.author?.avatar || '') : '',
3285
+ template: cfg.urlTemplate,
3286
+ size: Number(cfg.size) > 0 ? Number(cfg.size) : 640,
3287
+ })
3288
+ if (url) return url
3289
+
3290
+ // 内置规则拼不出来(Discord / KOOK / 频道类平台)时,让适配器直接给
3291
+ try {
3292
+ const member = session.guildId
3293
+ ? await (session.bot as any)?.getGuildMember?.(session.guildId, userId)
3294
+ : null
3295
+ const got = member?.avatar || member?.user?.avatar
3296
+ if (got) return String(got)
3297
+ } catch (error) {
3298
+ logInfo(`向适配器索取头像失败: ${error}`)
3299
+ }
3300
+ return ''
3301
+ }
3302
+
2990
3303
  function extractImagesFromSession(session: Session): string[] {
2991
3304
  const images: string[] = []
2992
3305