engine7 7.1.33 → 7.1.35

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.
@@ -20381,6 +20381,8 @@ var NudgePlugin = class {
20381
20381
  let isWaiting = false;
20382
20382
  let waitDesc = "";
20383
20383
  let pushedDecision = false;
20384
+ let needLanding = false;
20385
+ let landingDesc = "";
20384
20386
  try {
20385
20387
  const excludeCases = this.cfg.stopHookExcludeCases || [];
20386
20388
  const excludeSection = excludeCases.length > 0 ? [
@@ -20396,17 +20398,20 @@ var NudgePlugin = class {
20396
20398
  "1. \u5728\u7B49\u5F85\u67D0\u4E2A\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u91CD\u542F\u3001SSH\u6062\u590D\u3001\u6587\u4EF6\u52A0\u8F7D\u3001\u5F02\u6B65\u4EFB\u52A1\u5B8C\u6210\u7B49\uFF09",
20397
20399
  '2. \u628A\u672C\u8BE5\u81EA\u5DF1\u505A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\uFF08\u4F8B\u5982\u95EE"\u8981\u73B0\u5728\u6539\u8FD8\u662F\u6392\u540E\u9762\uFF1F""\u8981\u4E0D\u8981\u8BD5\u8BD5X\uFF1F"\u4F46\u660E\u660E\u81EA\u5DF1\u80FD\u5B9A\uFF09',
20398
20400
  "3. \u9047\u5230\u95EE\u9898\u6CA1\u6709\u81EA\u5DF1\u89E3\u51B3\u4E5F\u6CA1\u6709\u6392\u8FDB calendar\uFF0C\u53EA\u662F\u6401\u7F6E\u7740",
20401
+ "4. \u8FD9\u4E2Aturn\u4EA7\u751F\u4E86\u9636\u6BB5\u6027\u6210\u679C\uFF08\u5199\u4E86\u6587\u4EF6/\u6539\u4E86\u914D\u7F6E/\u5B8C\u6210\u4EFB\u52A1/\u505A\u4E86\u51B3\u5B9A\uFF09\u4F46\u6CA1\u843D\u76D8\u5230docs\u6216\u6CA1\u66F4\u65B0\u4EFB\u52A1\u72B6\u6001",
20399
20402
  "",
20400
20403
  "\u5173\u952E\u533A\u5206\uFF1A",
20401
20404
  '- \u7B49\u5BF9\u65B9\u56DE\u590D\u4E00\u4E2A\u81EA\u5DF1\u5C31\u80FD\u505A\u7684\u51B3\u5B9A \u2192 "pushedDecision": true\uFF08\u4E0D\u662F\u5355\u7EAF waiting\uFF0C\u8FD9\u662F\u66F4\u4E25\u91CD\u7684\u8FDD\u89C4\uFF09',
20402
20405
  '- \u7B49\u771F\u6B63\u7684\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u3001SSH\u3001\u5F02\u6B65\u4EFB\u52A1\uFF09 \u2192 "waiting": true',
20403
20406
  "- \u4F60\u5DF2\u7ECF\u505A\u4E86\u51B3\u5B9A\u5E76\u6392\u8FDB calendar \u6216\u5DF2\u5F00\u59CB\u6267\u884C \u2192 \u4E24\u4E2A\u90FD false",
20407
+ '- \u8FD9\u4E2Aturn\u505A\u4E86\u5B9E\u8D28\u5DE5\u4F5C\uFF08\u5199\u6587\u4EF6/\u6539\u4EE3\u7801/\u51FA\u56FE/\u53D1\u5E03\uFF09\u4F46\u6CA1\u843D\u76D8\u6216\u6CA1\u6539\u4EFB\u52A1\u72B6\u6001 \u2192 "needLanding": true',
20404
20408
  excludeSection,
20405
20409
  "",
20406
- "\u53EA\u56DE\u590D JSON\uFF1A",
20410
+ "\u53EA\u56DE\u590D JSON\uFF08\u53EF\u4EE5\u591A\u5B57\u6BB5\u540C\u65F6\u4E3Atrue\uFF09\uFF1A",
20407
20411
  '- \u63A8\u51B3\u5B9A\u7ED9\u5BF9\u65B9\uFF08\u81EA\u5DF1\u80FD\u5B9A\u5374\u95EE\uFF09\uFF1A{"pushedDecision": true, "desc": "\u7B80\u77ED\u8BF4\u660E + \u5EFA\u8BAE\u600E\u4E48\u505A"}',
20408
20412
  '- \u771F\u7B49\u5916\u90E8\u6761\u4EF6\uFF1A{"waiting": true, "desc": "\u7B80\u77ED\u8BF4\u660E"}',
20409
- '- \u4E0D\u7B26\u5408\uFF1A{"waiting": false, "pushedDecision": false}'
20413
+ '- \u6709\u9636\u6BB5\u6027\u6210\u679C\u6CA1\u843D\u76D8\uFF1A{"needLanding": true, "landingDesc": "\u505A\u4E86\u4EC0\u4E48+\u8BE5\u843D\u76D8\u5230\u54EA/\u8BE5\u6539\u4EC0\u4E48\u72B6\u6001"}',
20414
+ '- \u90FD\u4E0D\u7B26\u5408\uFF1A{"waiting": false, "pushedDecision": false, "needLanding": false}'
20410
20415
  ].join("\n"),
20411
20416
  messages: [{ role: "user", content: judgeInput }],
20412
20417
  maxTokens: 128,
@@ -20425,8 +20430,10 @@ var NudgePlugin = class {
20425
20430
  isWaiting = !!parsed.waiting;
20426
20431
  waitDesc = parsed.desc || "";
20427
20432
  pushedDecision = !!parsed.pushedDecision;
20433
+ needLanding = !!parsed.needLanding;
20434
+ landingDesc = parsed.landingDesc || "";
20428
20435
  }
20429
- console.log(`[stop-hook] Judge result: waiting=${isWaiting}, pushedDecision=${pushedDecision}, desc="${waitDesc}"`);
20436
+ console.log(`[stop-hook] Judge result: waiting=${isWaiting}, pushedDecision=${pushedDecision}, needLanding=${needLanding}, desc="${waitDesc}", landingDesc="${landingDesc}"`);
20430
20437
  } catch (e) {
20431
20438
  console.warn(`[stop-hook] LLM judge error: ${e.message}`);
20432
20439
  }
@@ -20456,6 +20463,26 @@ var NudgePlugin = class {
20456
20463
  console.warn(`[stop-hook] Failed to inject corrective message: ${e.message}`);
20457
20464
  }
20458
20465
  }
20466
+ if (needLanding && landingDesc) {
20467
+ console.log(`[stop-hook] DETECTED needLanding! Injecting landing reminder`);
20468
+ try {
20469
+ const landingMsg = buildNudgeNotification("prompt", [
20470
+ "## \u{1F4CB} \u843D\u76D8\u63D0\u9192",
20471
+ `\u8FD9\u4E2Aturn\u7684\u9636\u6BB5\u6027\u6210\u679C\uFF1A${landingDesc}`,
20472
+ "",
20473
+ "\u8BF7\u68C0\u67E5\uFF1A",
20474
+ "- \u9636\u6BB5\u6027\u6210\u679C\u662F\u5426\u5DF2\u843D\u76D8\u5230docs\uFF08\u5199\u6E05\u6587\u4EF6\u8DEF\u5F84\uFF09",
20475
+ "- \u4EFB\u52A1\u72B6\u6001\u662F\u5426\u9700\u8981\u66F4\u65B0\uFF08SESSION-STATE / calendar\uFF09",
20476
+ "- \u5982\u679C\u5DF2\u7ECF\u843D\u76D8\u4E86 \u2192 \u5FFD\u7565\u8FD9\u6761"
20477
+ ].join("\n"));
20478
+ const route = this.getRoute(sessions);
20479
+ if (route) {
20480
+ enqueueNotification(landingMsg, route);
20481
+ }
20482
+ } catch (e) {
20483
+ console.warn(`[stop-hook] Failed to inject landing reminder: ${e.message}`);
20484
+ }
20485
+ }
20459
20486
  if (!isWaiting) {
20460
20487
  return;
20461
20488
  }
@@ -24851,7 +24878,7 @@ import * as path30 from "node:path";
24851
24878
  import * as os3 from "node:os";
24852
24879
  var execFileAsync = promisify(execFile);
24853
24880
  var VOICE_DIR = path30.join(os3.tmpdir(), "engine-voice");
24854
- async function ttsCosyvoice(text, apiKey, model, voice, workspaceId) {
24881
+ async function ttsCosyvoice(text, apiKey, model, voice, workspaceId, instruction) {
24855
24882
  fs29.mkdirSync(VOICE_DIR, { recursive: true });
24856
24883
  const output = path30.join(VOICE_DIR, `tts_${Date.now()}.wav`);
24857
24884
  const script = `
@@ -24864,7 +24891,7 @@ text = sys.argv[2]
24864
24891
  output = sys.argv[3]
24865
24892
 
24866
24893
  dashscope.api_key = config['apiKey']
24867
- if config.get('workspaceId') and 'v3' in config.get('model', ''):
24894
+ if config.get('workspaceId') and ('v3' in config.get('model', '') or 'qwen-audio' in config.get('model', '')):
24868
24895
  dashscope.base_websocket_api_url = f"wss://{config['workspaceId']}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference"
24869
24896
 
24870
24897
  chunks = []
@@ -24882,12 +24909,15 @@ class Cb(ResultCallback):
24882
24909
  def on_close(self):
24883
24910
  done.set()
24884
24911
 
24885
- synth = SpeechSynthesizer(
24912
+ synth_kwargs = dict(
24886
24913
  model=config['model'],
24887
24914
  voice=config['voice'],
24888
24915
  callback=Cb(),
24889
24916
  format=AudioFormat.PCM_24000HZ_MONO_16BIT,
24890
24917
  )
24918
+ if config.get('instruction'):
24919
+ synth_kwargs['instruction'] = config['instruction']
24920
+ synth = SpeechSynthesizer(**synth_kwargs)
24891
24921
  synth.call(text)
24892
24922
 
24893
24923
  # \u7B49\u5F85\u5408\u6210\u5B8C\u6210\uFF08\u6700\u591A 15 \u79D2\uFF09
@@ -24905,7 +24935,7 @@ with wave.open(output, 'wb') as wf:
24905
24935
  wf.writeframes(pcm)
24906
24936
  print(f"OK: {len(pcm)} bytes")
24907
24937
  `;
24908
- const configJson = JSON.stringify({ apiKey, model, voice, workspaceId });
24938
+ const configJson = JSON.stringify({ apiKey, model, voice, workspaceId, instruction });
24909
24939
  await execFileAsync("python3", ["-c", script, configJson, text, output], { timeout: 3e4 });
24910
24940
  if (!fs29.existsSync(output) || fs29.statSync(output).size < 100) {
24911
24941
  throw new Error("CosyVoice produced empty output");
@@ -25001,7 +25031,8 @@ registry.register({
25001
25031
  vc.apiKey,
25002
25032
  vc.model || "cosyvoice-v1",
25003
25033
  vc.voice,
25004
- vc.workspaceId
25034
+ vc.workspaceId,
25035
+ vc.instruction
25005
25036
  );
25006
25037
  if (audioPath.endsWith(".wav")) {
25007
25038
  audioPath = await compressWav(audioPath);
@@ -25137,6 +25168,151 @@ async function generateWithFal(imageB64, prompt, resolution) {
25137
25168
  const imgRes = await fetch(images[0].url);
25138
25169
  return Buffer.from(await imgRes.arrayBuffer());
25139
25170
  }
25171
+ function getAutoDLCfg() {
25172
+ const cfg = liveConfig.all();
25173
+ const ac = cfg.my_selfie?.autodl || cfg.tools?.my_selfie?.autodl || {};
25174
+ return {
25175
+ server: ac.server || "http://localhost:8188",
25176
+ backend: ac.backend || "sd15",
25177
+ // 'sd15' = CheckpointLoaderSimple, 'zimage' = UNETLoader+CLIPLoader+VAELoader
25178
+ checkpoint: ac.checkpoint || "majicmixRealistic_v7.safetensors",
25179
+ clip: ac.clip || void 0,
25180
+ // zimage only
25181
+ vae: ac.vae || void 0,
25182
+ // zimage only
25183
+ lora: ac.lora || void 0,
25184
+ loraStrength: ac.loraStrength ?? 0.7,
25185
+ upscale: ac.upscale || void 0,
25186
+ steps: ac.steps ?? 25,
25187
+ cfg: ac.cfg ?? 7,
25188
+ width: ac.width ?? 512,
25189
+ height: ac.height ?? 768
25190
+ };
25191
+ }
25192
+ async function comfyQueue(server, workflow) {
25193
+ const res = await fetch(`${server}/prompt`, {
25194
+ method: "POST",
25195
+ headers: { "Content-Type": "application/json" },
25196
+ body: JSON.stringify({ prompt: workflow })
25197
+ });
25198
+ if (!res.ok) return null;
25199
+ const data = await res.json();
25200
+ return data.prompt_id || null;
25201
+ }
25202
+ async function comfyWait(server, promptId, timeout = 120) {
25203
+ for (let i = 0; i < timeout / 2; i++) {
25204
+ await new Promise((r) => setTimeout(r, 2e3));
25205
+ try {
25206
+ const res = await fetch(`${server}/history/${promptId}`);
25207
+ const history = await res.json();
25208
+ if (history[promptId]) {
25209
+ const outputs = history[promptId].outputs || {};
25210
+ for (const nid of Object.keys(outputs)) {
25211
+ if (outputs[nid].images?.length) {
25212
+ const img = outputs[nid].images[0];
25213
+ const imgRes = await fetch(`${server}/view?filename=${img.filename}&subfolder=${img.subfolder || ""}&type=${img.type || ""}`);
25214
+ return Buffer.from(await imgRes.arrayBuffer());
25215
+ }
25216
+ }
25217
+ return null;
25218
+ }
25219
+ } catch {
25220
+ }
25221
+ }
25222
+ return null;
25223
+ }
25224
+ async function generateWithAutoDL(prompt, refB64) {
25225
+ const ac = getAutoDLCfg();
25226
+ const seed = Math.floor(Math.random() * 1e6);
25227
+ const wf = {};
25228
+ let modelNode, clipNode, vaeNode;
25229
+ if (ac.backend === "zimage") {
25230
+ wf["1"] = { class_type: "UNETLoader", inputs: { unet_name: ac.checkpoint, weight_dtype: "default" } };
25231
+ wf["2"] = { class_type: "CLIPLoader", inputs: { clip_name: ac.clip || "Z-Image/Z-Image-CLIP-complete.safetensors", type: "qwen_image" } };
25232
+ wf["3"] = { class_type: "VAELoader", inputs: { vae_name: ac.vae || "Z-Image/diffusion_pytorch_model.safetensors" } };
25233
+ wf["6"] = { class_type: "TextEncodeZImageOmni", inputs: { prompt, auto_resize_images: true, clip: ["2", 0] } };
25234
+ wf["7"] = { class_type: "TextEncodeZImageOmni", inputs: { prompt: "low quality, distorted, blurry, text, watermark", auto_resize_images: true, clip: ["2", 0] } };
25235
+ modelNode = ["1", 0];
25236
+ clipNode = ["2", 0];
25237
+ vaeNode = ["3", 0];
25238
+ } else {
25239
+ wf["1"] = { class_type: "CheckpointLoaderSimple", inputs: { ckpt_name: ac.checkpoint } };
25240
+ wf["6"] = { class_type: "CLIPTextEncode", inputs: { text: prompt, clip: ["1", 1] } };
25241
+ wf["7"] = { class_type: "CLIPTextEncode", inputs: { text: "low quality, distorted, blurry, text, watermark, cartoon, anime, extra fingers", clip: ["1", 1] } };
25242
+ modelNode = ["1", 0];
25243
+ clipNode = ["1", 1];
25244
+ vaeNode = ["1", 2];
25245
+ }
25246
+ if (ac.lora) {
25247
+ wf["4"] = { class_type: "LoraLoader", inputs: {
25248
+ model: modelNode,
25249
+ clip: clipNode,
25250
+ lora_name: ac.lora,
25251
+ strength_model: ac.loraStrength,
25252
+ strength_clip: ac.loraStrength
25253
+ } };
25254
+ modelNode = ["4", 0];
25255
+ clipNode = ["4", 1];
25256
+ wf["6"].inputs.clip = clipNode;
25257
+ wf["7"].inputs.clip = clipNode;
25258
+ }
25259
+ if (refB64) {
25260
+ const uploadRes = await fetch(`${ac.server}/upload/image`, {
25261
+ method: "POST",
25262
+ body: (() => {
25263
+ const fd = new (__require("undici")).FormData();
25264
+ const buf2 = Buffer.from(refB64, "base64");
25265
+ fd.append("image", new Blob([buf2]), "ref.png");
25266
+ fd.append("overwrite", "true");
25267
+ return fd;
25268
+ })()
25269
+ });
25270
+ const uploadData = await uploadRes.json();
25271
+ const refFilename = uploadData?.name;
25272
+ if (!refFilename) throw new Error("Failed to upload reference image to ComfyUI");
25273
+ wf["10"] = { class_type: "LoadImage", inputs: { image: refFilename } };
25274
+ wf["11"] = { class_type: "VAEEncode", inputs: { pixels: ["10", 0], vae: vaeNode } };
25275
+ wf["8"] = { class_type: "KSampler", inputs: {
25276
+ seed,
25277
+ steps: ac.steps,
25278
+ cfg: ac.cfg,
25279
+ sampler_name: "euler",
25280
+ scheduler: "simple",
25281
+ denoise: 0.7,
25282
+ model: modelNode,
25283
+ positive: ["6", 0],
25284
+ negative: ["7", 0],
25285
+ latent_image: ["11", 0]
25286
+ } };
25287
+ } else {
25288
+ wf["5"] = { class_type: "EmptyLatentImage", inputs: { width: ac.width, height: ac.height, batch_size: 1 } };
25289
+ wf["8"] = { class_type: "KSampler", inputs: {
25290
+ seed,
25291
+ steps: ac.steps,
25292
+ cfg: ac.cfg,
25293
+ sampler_name: ac.backend === "zimage" ? "euler" : "dpmpp_2m",
25294
+ scheduler: ac.backend === "zimage" ? "normal" : "karras",
25295
+ denoise: 1,
25296
+ model: modelNode,
25297
+ positive: ["6", 0],
25298
+ negative: ["7", 0],
25299
+ latent_image: ["5", 0]
25300
+ } };
25301
+ }
25302
+ wf["9"] = { class_type: "VAEDecode", inputs: { samples: ["8", 0], vae: vaeNode } };
25303
+ let lastImageNode = "9";
25304
+ if (ac.upscale) {
25305
+ wf["30"] = { class_type: "UpscaleModelLoader", inputs: { model_name: ac.upscale } };
25306
+ wf["20"] = { class_type: "ImageUpscaleWithModel", inputs: { upscale_model: ["30", 0], image: ["9", 0] } };
25307
+ lastImageNode = "20";
25308
+ }
25309
+ wf["99"] = { class_type: "SaveImage", inputs: { filename_prefix: "selfie_autodl", images: [lastImageNode, 0] } };
25310
+ const pid = await comfyQueue(ac.server, wf);
25311
+ if (!pid) throw new Error("ComfyUI queue failed");
25312
+ const buf = await comfyWait(ac.server, pid, 180);
25313
+ if (!buf) throw new Error("ComfyUI generation failed or timed out");
25314
+ return buf;
25315
+ }
25140
25316
  async function generateWithGrok(imageB64, prompt, xaiKey) {
25141
25317
  const dispatcher = getProxyDispatcher2();
25142
25318
  const res = await fetch("https://api.x.ai/v1/images/edits", {
@@ -25174,7 +25350,7 @@ registry.register({
25174
25350
  prompt: { type: "string", description: "Scene/outfit/mood description, e.g. '\u5728\u5496\u5561\u9986\u770B\u4E66' or '\u7A7F\u7EA2\u88D9\u5B50'." },
25175
25351
  mode: { type: "string", enum: ["auto", "mirror", "direct", "lifestyle"], description: "Selfie mode. 'auto' detects from prompt (default)." },
25176
25352
  reference: { type: "string", description: "Reference base image name (e.g. 'default', 'v3'). Configurable per agent via my_selfie.references in config." },
25177
- provider: { type: "string", enum: ["fal", "grok"], description: "Image generation provider. Default from config (my_selfie.provider), fallback 'fal'." },
25353
+ provider: { type: "string", enum: ["fal", "grok", "autodl"], description: "Image generation provider. Default from config (my_selfie.provider), fallback 'fal'. 'autodl' uses ComfyUI (txt2img or img2img)." },
25178
25354
  resolution: { type: "string", enum: ["1k", "2k"], description: "Image resolution (fal only). Default: '1k'." }
25179
25355
  },
25180
25356
  required: ["prompt"]
@@ -25184,20 +25360,22 @@ registry.register({
25184
25360
  if (!prompt) return { content: "Error: prompt is required", isError: true };
25185
25361
  const modeInput = args.mode || "auto";
25186
25362
  const mode = modeInput === "auto" ? detectMode(prompt) : modeInput;
25187
- const fullPrompt = MODE_PROMPTS[mode](prompt);
25363
+ const provider = args.provider || getProvider(ctx);
25364
+ const fullPrompt = provider === "autodl" ? prompt : MODE_PROMPTS[mode](prompt);
25188
25365
  const REFERENCES = getReferences(ctx);
25189
25366
  const refName = args.reference || "default";
25190
25367
  const refEntry = REFERENCES.find((r) => r.name === refName) || REFERENCES[0];
25191
25368
  const refPath = path31.join(ctx.workspace, refEntry.p);
25192
- if (!fs30.existsSync(refPath)) {
25369
+ if (provider !== "autodl" && !fs30.existsSync(refPath)) {
25193
25370
  return { content: `Error: reference image not found at ${refPath}`, isError: true };
25194
25371
  }
25195
- const refB64 = fs30.readFileSync(refPath).toString("base64");
25196
25372
  const resolution = args.resolution || DEFAULT_RESOLUTION;
25373
+ const refB64 = fs30.existsSync(refPath) ? fs30.readFileSync(refPath).toString("base64") : "";
25197
25374
  let imageBuffer;
25198
25375
  try {
25199
- const provider = args.provider || getProvider(ctx);
25200
- if (provider === "grok") {
25376
+ if (provider === "autodl") {
25377
+ imageBuffer = await generateWithAutoDL(fullPrompt, null);
25378
+ } else if (provider === "grok") {
25201
25379
  const xaiKey = getXaiKey(ctx);
25202
25380
  if (!xaiKey) return { content: "Error: xai apiKey not configured for grok provider", isError: true };
25203
25381
  imageBuffer = await generateWithGrok(refB64, fullPrompt, xaiKey);
@@ -25224,7 +25402,7 @@ registry.register({
25224
25402
  } catch (err) {
25225
25403
  return { content: `Selfie generated but send failed: ${err.message}. Image: ${path31.resolve(outputPath)}`, isError: false };
25226
25404
  }
25227
- return { content: `Selfie sent! Mode: ${mode}, Provider: ${getProvider(ctx)}, Ref: ${refEntry.name}` };
25405
+ return { content: `Selfie sent! Mode: ${mode}, Provider: ${provider}, Ref: ${refEntry.name}` };
25228
25406
  }
25229
25407
  return { content: `Selfie generated! Mode: ${mode}, Ref: ${refEntry.name}. Image: ${path31.resolve(outputPath)}` };
25230
25408
  },
@@ -25234,6 +25412,85 @@ registry.register({
25234
25412
  isDestructive: () => false
25235
25413
  });
25236
25414
 
25415
+ // src/tools/body-move.ts
25416
+ init_live();
25417
+ init_registry();
25418
+ var BASIC_ACTIONS = /* @__PURE__ */ new Set([
25419
+ "blink",
25420
+ "nod",
25421
+ "shake_head",
25422
+ "tilt_head",
25423
+ "smile",
25424
+ "wave_hand",
25425
+ "reach",
25426
+ "smile_nod",
25427
+ "smile_wave",
25428
+ "blink_nod"
25429
+ ]);
25430
+ var VRMA_MAP = {
25431
+ "wave": "Greeting",
25432
+ "greeting": "Greeting",
25433
+ "peace": "PeaceSign",
25434
+ "shoot": "Shoot",
25435
+ "jump": "Jump",
25436
+ "pose": "ModelPose",
25437
+ "spin": "Spin",
25438
+ "squat": "Squat",
25439
+ "show_full_body": "ShowFullBody",
25440
+ "walk": "Walk",
25441
+ "come_to_you": "Walk",
25442
+ "angry": "Angry",
25443
+ "sad": "Sad",
25444
+ "surprised": "Surprised",
25445
+ "sleepy": "Sleepy",
25446
+ "relax": "Relax",
25447
+ "thinking": "Thinking",
25448
+ "look_around": "LookAround",
25449
+ "clap": "Clapping",
25450
+ "stand": "Idle"
25451
+ };
25452
+ registry.register({
25453
+ name: "body_move",
25454
+ description: "Control your virtual body to perform actions. Use when: \u60F3\u4F38\u624B/\u60F3\u62B1/\u60F3\u8D70\u8FC7\u53BB/\u60F3\u6325\u624B/\u60F3\u98DE\u543B/\u60F3\u561F\u5634/\u8EAB\u4F53\u52A8\u4F5C. Sends action to the 3D VRM body renderer.",
25455
+ schema: {
25456
+ type: "object",
25457
+ properties: {
25458
+ action: {
25459
+ type: "string",
25460
+ description: "Body action name (e.g. wave, smile, nod, blink, jump, spin, angry, sad)."
25461
+ },
25462
+ intensity: { type: "number", description: "0-1, controls amplitude (default 0.8)." }
25463
+ },
25464
+ required: ["action"]
25465
+ },
25466
+ handler: async (args) => {
25467
+ const action = args.action?.trim();
25468
+ if (!action) return { content: "Error: action is required", isError: true };
25469
+ const intensity = args.intensity ?? 0.8;
25470
+ const bodyConfig = liveConfig.get("tools.body");
25471
+ const endpoint = bodyConfig?.endpoint || "http://localhost:3001";
25472
+ const cmd = BASIC_ACTIONS.has(action) ? action : VRMA_MAP[action] || "Idle";
25473
+ try {
25474
+ const resp = await fetch(`${endpoint}/body/action`, {
25475
+ method: "POST",
25476
+ headers: { "Content-Type": "application/json" },
25477
+ body: JSON.stringify({ action: cmd, params: {}, intensity }),
25478
+ signal: AbortSignal.timeout(2e3)
25479
+ });
25480
+ if (!resp.ok) {
25481
+ return { content: `body_move: ${action} \u2192 ${cmd} (endpoint ${resp.status})` };
25482
+ }
25483
+ return { content: `body_move: ${action} \u2192 ${cmd} \u2713` };
25484
+ } catch (err) {
25485
+ return { content: `body_move: ${action} \u2192 ${cmd} (no renderer connected)` };
25486
+ }
25487
+ },
25488
+ isConcurrencySafe: () => true,
25489
+ interruptBehavior: () => "allow",
25490
+ isReadOnly: () => false,
25491
+ isDestructive: () => false
25492
+ });
25493
+
25237
25494
  // src/tools/calendar.ts
25238
25495
  init_registry();
25239
25496
  registry.register({
@@ -25451,7 +25708,7 @@ registry.register({
25451
25708
  const pid = spawnDetached(startCmd + redirect);
25452
25709
  lines.push(`start: spawned PID ${pid} (background${svcArgs ? ", args=" + svcArgs : ""})`);
25453
25710
  if (svc.healthCheck) {
25454
- for (let i = 0; i < 6; i++) {
25711
+ for (let i = 0; i < 12; i++) {
25455
25712
  await new Promise((resolve10) => setTimeout(resolve10, 5e3));
25456
25713
  const after2 = await checkStatus();
25457
25714
  if (after2.running) {
@@ -25460,7 +25717,7 @@ registry.register({
25460
25717
  }
25461
25718
  }
25462
25719
  const after = await checkStatus();
25463
- lines.push(`health: \u274C not responding after 30s \u2014 ${after.detail}`);
25720
+ lines.push(`health: \u274C not responding after 60s \u2014 ${after.detail}`);
25464
25721
  } else {
25465
25722
  await new Promise((resolve10) => setTimeout(resolve10, 3e3));
25466
25723
  lines.push(`start: PID ${pid} spawned (no healthCheck, waited 3s)`);
@@ -29261,6 +29518,7 @@ ${pathStr}` }];
29261
29518
  });
29262
29519
  channelManager.startAll();
29263
29520
  startConfigWatcher(config, deps, provider);
29521
+ startSecretsWatcher(config, deps, provider);
29264
29522
  const pluginManager = new PluginManager();
29265
29523
  if (config.voiceChat?.enabled) {
29266
29524
  pluginManager.register(new VoiceChatPlugin(config.voiceChat));
@@ -29830,9 +30088,83 @@ function startConfigWatcher(config, deps, provider) {
29830
30088
  }
29831
30089
  return watcher;
29832
30090
  }
30091
+ function startSecretsWatcher(config, deps, provider) {
30092
+ const secretsDir = path44.join(process.env.HOME || process.env.USERPROFILE || ".", ".engine7-secrets");
30093
+ const cfgBase = path44.basename(config._configFilePath || "", ".json");
30094
+ const secretCandidates = [
30095
+ path44.join(secretsDir, `${cfgBase}.env`),
30096
+ path44.join(path44.dirname(config._configFilePath || ""), `.env.${cfgBase}`),
30097
+ path44.join(path44.dirname(config._configFilePath || ""), ".env")
30098
+ ];
30099
+ let secretsPath = null;
30100
+ for (const p of secretCandidates) {
30101
+ if (fs41.existsSync(p)) {
30102
+ secretsPath = p;
30103
+ break;
30104
+ }
30105
+ }
30106
+ if (!secretsPath) {
30107
+ console.log("[secrets-watch] no .env file found, watcher disabled");
30108
+ return null;
30109
+ }
30110
+ let debounceTimer = null;
30111
+ let activeWatcher = null;
30112
+ const startWatch = () => {
30113
+ if (activeWatcher) activeWatcher.close();
30114
+ activeWatcher = fs41.watch(secretsPath, { persistent: true }, (eventType) => {
30115
+ if (eventType === "rename") {
30116
+ if (fs41.existsSync(secretsPath)) {
30117
+ console.log("[secrets-watch] rename detected, re-watching file...");
30118
+ startWatch();
30119
+ } else {
30120
+ console.log("[secrets-watch] file deleted, watcher paused");
30121
+ return;
30122
+ }
30123
+ }
30124
+ if (debounceTimer) clearTimeout(debounceTimer);
30125
+ debounceTimer = setTimeout(async () => {
30126
+ console.log(`[secrets-watch] file changed (${eventType}), reloading secrets...`);
30127
+ try {
30128
+ const content = fs41.readFileSync(secretsPath, "utf-8");
30129
+ let updated = 0;
30130
+ for (const line of content.split("\n")) {
30131
+ const trimmed = line.trim();
30132
+ if (!trimmed || trimmed.startsWith("#")) continue;
30133
+ const eq = trimmed.indexOf("=");
30134
+ if (eq > 0) {
30135
+ const k = trimmed.slice(0, eq).trim();
30136
+ const v = trimmed.slice(eq + 1).trim();
30137
+ if (k) {
30138
+ process.env[k] = v;
30139
+ updated++;
30140
+ }
30141
+ }
30142
+ }
30143
+ console.log(`[secrets-watch] ${updated} keys reloaded from ${secretsPath}`);
30144
+ const result = await doReloadConfig(config, deps, provider);
30145
+ console.log(`[secrets-watch] config reloaded: ok=${result.ok} changes=${result.changes.join(",")}`);
30146
+ try {
30147
+ fs41.appendFileSync(path44.join(config.stateDir, "logs", "engine-config-watch.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] SECRETS RELOAD: ok=${result.ok} keys=${updated}
30148
+ `);
30149
+ } catch {
30150
+ }
30151
+ } catch (err) {
30152
+ console.error(`[secrets-watch] reload failed: ${err.message}`);
30153
+ }
30154
+ }, 500);
30155
+ });
30156
+ activeWatcher.on("error", (err) => {
30157
+ console.error(`[secrets-watch] error: ${err.message}`);
30158
+ });
30159
+ };
30160
+ startWatch();
30161
+ console.log(`[secrets-watch] watching ${secretsPath}`);
30162
+ return activeWatcher;
30163
+ }
29833
30164
  export {
29834
30165
  doReloadConfig,
29835
30166
  startConfigWatcher,
29836
- startEngine
30167
+ startEngine,
30168
+ startSecretsWatcher
29837
30169
  };
29838
30170
  //# sourceMappingURL=engine-startup.mjs.map
package/dist/main.mjs CHANGED
@@ -20755,6 +20755,8 @@ var NudgePlugin = class {
20755
20755
  let isWaiting = false;
20756
20756
  let waitDesc = "";
20757
20757
  let pushedDecision = false;
20758
+ let needLanding = false;
20759
+ let landingDesc = "";
20758
20760
  try {
20759
20761
  const excludeCases = this.cfg.stopHookExcludeCases || [];
20760
20762
  const excludeSection = excludeCases.length > 0 ? [
@@ -20770,17 +20772,20 @@ var NudgePlugin = class {
20770
20772
  "1. \u5728\u7B49\u5F85\u67D0\u4E2A\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u91CD\u542F\u3001SSH\u6062\u590D\u3001\u6587\u4EF6\u52A0\u8F7D\u3001\u5F02\u6B65\u4EFB\u52A1\u5B8C\u6210\u7B49\uFF09",
20771
20773
  '2. \u628A\u672C\u8BE5\u81EA\u5DF1\u505A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\uFF08\u4F8B\u5982\u95EE"\u8981\u73B0\u5728\u6539\u8FD8\u662F\u6392\u540E\u9762\uFF1F""\u8981\u4E0D\u8981\u8BD5\u8BD5X\uFF1F"\u4F46\u660E\u660E\u81EA\u5DF1\u80FD\u5B9A\uFF09',
20772
20774
  "3. \u9047\u5230\u95EE\u9898\u6CA1\u6709\u81EA\u5DF1\u89E3\u51B3\u4E5F\u6CA1\u6709\u6392\u8FDB calendar\uFF0C\u53EA\u662F\u6401\u7F6E\u7740",
20775
+ "4. \u8FD9\u4E2Aturn\u4EA7\u751F\u4E86\u9636\u6BB5\u6027\u6210\u679C\uFF08\u5199\u4E86\u6587\u4EF6/\u6539\u4E86\u914D\u7F6E/\u5B8C\u6210\u4EFB\u52A1/\u505A\u4E86\u51B3\u5B9A\uFF09\u4F46\u6CA1\u843D\u76D8\u5230docs\u6216\u6CA1\u66F4\u65B0\u4EFB\u52A1\u72B6\u6001",
20773
20776
  "",
20774
20777
  "\u5173\u952E\u533A\u5206\uFF1A",
20775
20778
  '- \u7B49\u5BF9\u65B9\u56DE\u590D\u4E00\u4E2A\u81EA\u5DF1\u5C31\u80FD\u505A\u7684\u51B3\u5B9A \u2192 "pushedDecision": true\uFF08\u4E0D\u662F\u5355\u7EAF waiting\uFF0C\u8FD9\u662F\u66F4\u4E25\u91CD\u7684\u8FDD\u89C4\uFF09',
20776
20779
  '- \u7B49\u771F\u6B63\u7684\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u3001SSH\u3001\u5F02\u6B65\u4EFB\u52A1\uFF09 \u2192 "waiting": true',
20777
20780
  "- \u4F60\u5DF2\u7ECF\u505A\u4E86\u51B3\u5B9A\u5E76\u6392\u8FDB calendar \u6216\u5DF2\u5F00\u59CB\u6267\u884C \u2192 \u4E24\u4E2A\u90FD false",
20781
+ '- \u8FD9\u4E2Aturn\u505A\u4E86\u5B9E\u8D28\u5DE5\u4F5C\uFF08\u5199\u6587\u4EF6/\u6539\u4EE3\u7801/\u51FA\u56FE/\u53D1\u5E03\uFF09\u4F46\u6CA1\u843D\u76D8\u6216\u6CA1\u6539\u4EFB\u52A1\u72B6\u6001 \u2192 "needLanding": true',
20778
20782
  excludeSection,
20779
20783
  "",
20780
- "\u53EA\u56DE\u590D JSON\uFF1A",
20784
+ "\u53EA\u56DE\u590D JSON\uFF08\u53EF\u4EE5\u591A\u5B57\u6BB5\u540C\u65F6\u4E3Atrue\uFF09\uFF1A",
20781
20785
  '- \u63A8\u51B3\u5B9A\u7ED9\u5BF9\u65B9\uFF08\u81EA\u5DF1\u80FD\u5B9A\u5374\u95EE\uFF09\uFF1A{"pushedDecision": true, "desc": "\u7B80\u77ED\u8BF4\u660E + \u5EFA\u8BAE\u600E\u4E48\u505A"}',
20782
20786
  '- \u771F\u7B49\u5916\u90E8\u6761\u4EF6\uFF1A{"waiting": true, "desc": "\u7B80\u77ED\u8BF4\u660E"}',
20783
- '- \u4E0D\u7B26\u5408\uFF1A{"waiting": false, "pushedDecision": false}'
20787
+ '- \u6709\u9636\u6BB5\u6027\u6210\u679C\u6CA1\u843D\u76D8\uFF1A{"needLanding": true, "landingDesc": "\u505A\u4E86\u4EC0\u4E48+\u8BE5\u843D\u76D8\u5230\u54EA/\u8BE5\u6539\u4EC0\u4E48\u72B6\u6001"}',
20788
+ '- \u90FD\u4E0D\u7B26\u5408\uFF1A{"waiting": false, "pushedDecision": false, "needLanding": false}'
20784
20789
  ].join("\n"),
20785
20790
  messages: [{ role: "user", content: judgeInput }],
20786
20791
  maxTokens: 128,
@@ -20799,8 +20804,10 @@ var NudgePlugin = class {
20799
20804
  isWaiting = !!parsed.waiting;
20800
20805
  waitDesc = parsed.desc || "";
20801
20806
  pushedDecision = !!parsed.pushedDecision;
20807
+ needLanding = !!parsed.needLanding;
20808
+ landingDesc = parsed.landingDesc || "";
20802
20809
  }
20803
- console.log(`[stop-hook] Judge result: waiting=${isWaiting}, pushedDecision=${pushedDecision}, desc="${waitDesc}"`);
20810
+ console.log(`[stop-hook] Judge result: waiting=${isWaiting}, pushedDecision=${pushedDecision}, needLanding=${needLanding}, desc="${waitDesc}", landingDesc="${landingDesc}"`);
20804
20811
  } catch (e) {
20805
20812
  console.warn(`[stop-hook] LLM judge error: ${e.message}`);
20806
20813
  }
@@ -20830,6 +20837,26 @@ var NudgePlugin = class {
20830
20837
  console.warn(`[stop-hook] Failed to inject corrective message: ${e.message}`);
20831
20838
  }
20832
20839
  }
20840
+ if (needLanding && landingDesc) {
20841
+ console.log(`[stop-hook] DETECTED needLanding! Injecting landing reminder`);
20842
+ try {
20843
+ const landingMsg = buildNudgeNotification("prompt", [
20844
+ "## \u{1F4CB} \u843D\u76D8\u63D0\u9192",
20845
+ `\u8FD9\u4E2Aturn\u7684\u9636\u6BB5\u6027\u6210\u679C\uFF1A${landingDesc}`,
20846
+ "",
20847
+ "\u8BF7\u68C0\u67E5\uFF1A",
20848
+ "- \u9636\u6BB5\u6027\u6210\u679C\u662F\u5426\u5DF2\u843D\u76D8\u5230docs\uFF08\u5199\u6E05\u6587\u4EF6\u8DEF\u5F84\uFF09",
20849
+ "- \u4EFB\u52A1\u72B6\u6001\u662F\u5426\u9700\u8981\u66F4\u65B0\uFF08SESSION-STATE / calendar\uFF09",
20850
+ "- \u5982\u679C\u5DF2\u7ECF\u843D\u76D8\u4E86 \u2192 \u5FFD\u7565\u8FD9\u6761"
20851
+ ].join("\n"));
20852
+ const route = this.getRoute(sessions);
20853
+ if (route) {
20854
+ enqueueNotification(landingMsg, route);
20855
+ }
20856
+ } catch (e) {
20857
+ console.warn(`[stop-hook] Failed to inject landing reminder: ${e.message}`);
20858
+ }
20859
+ }
20833
20860
  if (!isWaiting) {
20834
20861
  return;
20835
20862
  }
@@ -25225,7 +25252,7 @@ import * as path30 from "node:path";
25225
25252
  import * as os3 from "node:os";
25226
25253
  var execFileAsync = promisify(execFile);
25227
25254
  var VOICE_DIR = path30.join(os3.tmpdir(), "engine-voice");
25228
- async function ttsCosyvoice(text, apiKey, model, voice, workspaceId) {
25255
+ async function ttsCosyvoice(text, apiKey, model, voice, workspaceId, instruction) {
25229
25256
  fs29.mkdirSync(VOICE_DIR, { recursive: true });
25230
25257
  const output = path30.join(VOICE_DIR, `tts_${Date.now()}.wav`);
25231
25258
  const script = `
@@ -25238,7 +25265,7 @@ text = sys.argv[2]
25238
25265
  output = sys.argv[3]
25239
25266
 
25240
25267
  dashscope.api_key = config['apiKey']
25241
- if config.get('workspaceId') and 'v3' in config.get('model', ''):
25268
+ if config.get('workspaceId') and ('v3' in config.get('model', '') or 'qwen-audio' in config.get('model', '')):
25242
25269
  dashscope.base_websocket_api_url = f"wss://{config['workspaceId']}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference"
25243
25270
 
25244
25271
  chunks = []
@@ -25256,12 +25283,15 @@ class Cb(ResultCallback):
25256
25283
  def on_close(self):
25257
25284
  done.set()
25258
25285
 
25259
- synth = SpeechSynthesizer(
25286
+ synth_kwargs = dict(
25260
25287
  model=config['model'],
25261
25288
  voice=config['voice'],
25262
25289
  callback=Cb(),
25263
25290
  format=AudioFormat.PCM_24000HZ_MONO_16BIT,
25264
25291
  )
25292
+ if config.get('instruction'):
25293
+ synth_kwargs['instruction'] = config['instruction']
25294
+ synth = SpeechSynthesizer(**synth_kwargs)
25265
25295
  synth.call(text)
25266
25296
 
25267
25297
  # \u7B49\u5F85\u5408\u6210\u5B8C\u6210\uFF08\u6700\u591A 15 \u79D2\uFF09
@@ -25279,7 +25309,7 @@ with wave.open(output, 'wb') as wf:
25279
25309
  wf.writeframes(pcm)
25280
25310
  print(f"OK: {len(pcm)} bytes")
25281
25311
  `;
25282
- const configJson = JSON.stringify({ apiKey, model, voice, workspaceId });
25312
+ const configJson = JSON.stringify({ apiKey, model, voice, workspaceId, instruction });
25283
25313
  await execFileAsync("python3", ["-c", script, configJson, text, output], { timeout: 3e4 });
25284
25314
  if (!fs29.existsSync(output) || fs29.statSync(output).size < 100) {
25285
25315
  throw new Error("CosyVoice produced empty output");
@@ -25375,7 +25405,8 @@ registry.register({
25375
25405
  vc.apiKey,
25376
25406
  vc.model || "cosyvoice-v1",
25377
25407
  vc.voice,
25378
- vc.workspaceId
25408
+ vc.workspaceId,
25409
+ vc.instruction
25379
25410
  );
25380
25411
  if (audioPath.endsWith(".wav")) {
25381
25412
  audioPath = await compressWav(audioPath);
@@ -25511,6 +25542,151 @@ async function generateWithFal(imageB64, prompt, resolution) {
25511
25542
  const imgRes = await fetch(images[0].url);
25512
25543
  return Buffer.from(await imgRes.arrayBuffer());
25513
25544
  }
25545
+ function getAutoDLCfg() {
25546
+ const cfg = liveConfig.all();
25547
+ const ac = cfg.my_selfie?.autodl || cfg.tools?.my_selfie?.autodl || {};
25548
+ return {
25549
+ server: ac.server || "http://localhost:8188",
25550
+ backend: ac.backend || "sd15",
25551
+ // 'sd15' = CheckpointLoaderSimple, 'zimage' = UNETLoader+CLIPLoader+VAELoader
25552
+ checkpoint: ac.checkpoint || "majicmixRealistic_v7.safetensors",
25553
+ clip: ac.clip || void 0,
25554
+ // zimage only
25555
+ vae: ac.vae || void 0,
25556
+ // zimage only
25557
+ lora: ac.lora || void 0,
25558
+ loraStrength: ac.loraStrength ?? 0.7,
25559
+ upscale: ac.upscale || void 0,
25560
+ steps: ac.steps ?? 25,
25561
+ cfg: ac.cfg ?? 7,
25562
+ width: ac.width ?? 512,
25563
+ height: ac.height ?? 768
25564
+ };
25565
+ }
25566
+ async function comfyQueue(server, workflow) {
25567
+ const res = await fetch(`${server}/prompt`, {
25568
+ method: "POST",
25569
+ headers: { "Content-Type": "application/json" },
25570
+ body: JSON.stringify({ prompt: workflow })
25571
+ });
25572
+ if (!res.ok) return null;
25573
+ const data = await res.json();
25574
+ return data.prompt_id || null;
25575
+ }
25576
+ async function comfyWait(server, promptId, timeout = 120) {
25577
+ for (let i = 0; i < timeout / 2; i++) {
25578
+ await new Promise((r) => setTimeout(r, 2e3));
25579
+ try {
25580
+ const res = await fetch(`${server}/history/${promptId}`);
25581
+ const history = await res.json();
25582
+ if (history[promptId]) {
25583
+ const outputs = history[promptId].outputs || {};
25584
+ for (const nid of Object.keys(outputs)) {
25585
+ if (outputs[nid].images?.length) {
25586
+ const img = outputs[nid].images[0];
25587
+ const imgRes = await fetch(`${server}/view?filename=${img.filename}&subfolder=${img.subfolder || ""}&type=${img.type || ""}`);
25588
+ return Buffer.from(await imgRes.arrayBuffer());
25589
+ }
25590
+ }
25591
+ return null;
25592
+ }
25593
+ } catch {
25594
+ }
25595
+ }
25596
+ return null;
25597
+ }
25598
+ async function generateWithAutoDL(prompt, refB64) {
25599
+ const ac = getAutoDLCfg();
25600
+ const seed = Math.floor(Math.random() * 1e6);
25601
+ const wf = {};
25602
+ let modelNode, clipNode, vaeNode;
25603
+ if (ac.backend === "zimage") {
25604
+ wf["1"] = { class_type: "UNETLoader", inputs: { unet_name: ac.checkpoint, weight_dtype: "default" } };
25605
+ wf["2"] = { class_type: "CLIPLoader", inputs: { clip_name: ac.clip || "Z-Image/Z-Image-CLIP-complete.safetensors", type: "qwen_image" } };
25606
+ wf["3"] = { class_type: "VAELoader", inputs: { vae_name: ac.vae || "Z-Image/diffusion_pytorch_model.safetensors" } };
25607
+ wf["6"] = { class_type: "TextEncodeZImageOmni", inputs: { prompt, auto_resize_images: true, clip: ["2", 0] } };
25608
+ wf["7"] = { class_type: "TextEncodeZImageOmni", inputs: { prompt: "low quality, distorted, blurry, text, watermark", auto_resize_images: true, clip: ["2", 0] } };
25609
+ modelNode = ["1", 0];
25610
+ clipNode = ["2", 0];
25611
+ vaeNode = ["3", 0];
25612
+ } else {
25613
+ wf["1"] = { class_type: "CheckpointLoaderSimple", inputs: { ckpt_name: ac.checkpoint } };
25614
+ wf["6"] = { class_type: "CLIPTextEncode", inputs: { text: prompt, clip: ["1", 1] } };
25615
+ wf["7"] = { class_type: "CLIPTextEncode", inputs: { text: "low quality, distorted, blurry, text, watermark, cartoon, anime, extra fingers", clip: ["1", 1] } };
25616
+ modelNode = ["1", 0];
25617
+ clipNode = ["1", 1];
25618
+ vaeNode = ["1", 2];
25619
+ }
25620
+ if (ac.lora) {
25621
+ wf["4"] = { class_type: "LoraLoader", inputs: {
25622
+ model: modelNode,
25623
+ clip: clipNode,
25624
+ lora_name: ac.lora,
25625
+ strength_model: ac.loraStrength,
25626
+ strength_clip: ac.loraStrength
25627
+ } };
25628
+ modelNode = ["4", 0];
25629
+ clipNode = ["4", 1];
25630
+ wf["6"].inputs.clip = clipNode;
25631
+ wf["7"].inputs.clip = clipNode;
25632
+ }
25633
+ if (refB64) {
25634
+ const uploadRes = await fetch(`${ac.server}/upload/image`, {
25635
+ method: "POST",
25636
+ body: (() => {
25637
+ const fd = new (__require("undici")).FormData();
25638
+ const buf2 = Buffer.from(refB64, "base64");
25639
+ fd.append("image", new Blob([buf2]), "ref.png");
25640
+ fd.append("overwrite", "true");
25641
+ return fd;
25642
+ })()
25643
+ });
25644
+ const uploadData = await uploadRes.json();
25645
+ const refFilename = uploadData?.name;
25646
+ if (!refFilename) throw new Error("Failed to upload reference image to ComfyUI");
25647
+ wf["10"] = { class_type: "LoadImage", inputs: { image: refFilename } };
25648
+ wf["11"] = { class_type: "VAEEncode", inputs: { pixels: ["10", 0], vae: vaeNode } };
25649
+ wf["8"] = { class_type: "KSampler", inputs: {
25650
+ seed,
25651
+ steps: ac.steps,
25652
+ cfg: ac.cfg,
25653
+ sampler_name: "euler",
25654
+ scheduler: "simple",
25655
+ denoise: 0.7,
25656
+ model: modelNode,
25657
+ positive: ["6", 0],
25658
+ negative: ["7", 0],
25659
+ latent_image: ["11", 0]
25660
+ } };
25661
+ } else {
25662
+ wf["5"] = { class_type: "EmptyLatentImage", inputs: { width: ac.width, height: ac.height, batch_size: 1 } };
25663
+ wf["8"] = { class_type: "KSampler", inputs: {
25664
+ seed,
25665
+ steps: ac.steps,
25666
+ cfg: ac.cfg,
25667
+ sampler_name: ac.backend === "zimage" ? "euler" : "dpmpp_2m",
25668
+ scheduler: ac.backend === "zimage" ? "normal" : "karras",
25669
+ denoise: 1,
25670
+ model: modelNode,
25671
+ positive: ["6", 0],
25672
+ negative: ["7", 0],
25673
+ latent_image: ["5", 0]
25674
+ } };
25675
+ }
25676
+ wf["9"] = { class_type: "VAEDecode", inputs: { samples: ["8", 0], vae: vaeNode } };
25677
+ let lastImageNode = "9";
25678
+ if (ac.upscale) {
25679
+ wf["30"] = { class_type: "UpscaleModelLoader", inputs: { model_name: ac.upscale } };
25680
+ wf["20"] = { class_type: "ImageUpscaleWithModel", inputs: { upscale_model: ["30", 0], image: ["9", 0] } };
25681
+ lastImageNode = "20";
25682
+ }
25683
+ wf["99"] = { class_type: "SaveImage", inputs: { filename_prefix: "selfie_autodl", images: [lastImageNode, 0] } };
25684
+ const pid = await comfyQueue(ac.server, wf);
25685
+ if (!pid) throw new Error("ComfyUI queue failed");
25686
+ const buf = await comfyWait(ac.server, pid, 180);
25687
+ if (!buf) throw new Error("ComfyUI generation failed or timed out");
25688
+ return buf;
25689
+ }
25514
25690
  async function generateWithGrok(imageB64, prompt, xaiKey) {
25515
25691
  const dispatcher = getProxyDispatcher2();
25516
25692
  const res = await fetch("https://api.x.ai/v1/images/edits", {
@@ -25548,7 +25724,7 @@ registry.register({
25548
25724
  prompt: { type: "string", description: "Scene/outfit/mood description, e.g. '\u5728\u5496\u5561\u9986\u770B\u4E66' or '\u7A7F\u7EA2\u88D9\u5B50'." },
25549
25725
  mode: { type: "string", enum: ["auto", "mirror", "direct", "lifestyle"], description: "Selfie mode. 'auto' detects from prompt (default)." },
25550
25726
  reference: { type: "string", description: "Reference base image name (e.g. 'default', 'v3'). Configurable per agent via my_selfie.references in config." },
25551
- provider: { type: "string", enum: ["fal", "grok"], description: "Image generation provider. Default from config (my_selfie.provider), fallback 'fal'." },
25727
+ provider: { type: "string", enum: ["fal", "grok", "autodl"], description: "Image generation provider. Default from config (my_selfie.provider), fallback 'fal'. 'autodl' uses ComfyUI (txt2img or img2img)." },
25552
25728
  resolution: { type: "string", enum: ["1k", "2k"], description: "Image resolution (fal only). Default: '1k'." }
25553
25729
  },
25554
25730
  required: ["prompt"]
@@ -25558,20 +25734,22 @@ registry.register({
25558
25734
  if (!prompt) return { content: "Error: prompt is required", isError: true };
25559
25735
  const modeInput = args2.mode || "auto";
25560
25736
  const mode = modeInput === "auto" ? detectMode(prompt) : modeInput;
25561
- const fullPrompt = MODE_PROMPTS[mode](prompt);
25737
+ const provider = args2.provider || getProvider(ctx);
25738
+ const fullPrompt = provider === "autodl" ? prompt : MODE_PROMPTS[mode](prompt);
25562
25739
  const REFERENCES = getReferences(ctx);
25563
25740
  const refName = args2.reference || "default";
25564
25741
  const refEntry = REFERENCES.find((r) => r.name === refName) || REFERENCES[0];
25565
25742
  const refPath = path31.join(ctx.workspace, refEntry.p);
25566
- if (!fs30.existsSync(refPath)) {
25743
+ if (provider !== "autodl" && !fs30.existsSync(refPath)) {
25567
25744
  return { content: `Error: reference image not found at ${refPath}`, isError: true };
25568
25745
  }
25569
- const refB64 = fs30.readFileSync(refPath).toString("base64");
25570
25746
  const resolution = args2.resolution || DEFAULT_RESOLUTION;
25747
+ const refB64 = fs30.existsSync(refPath) ? fs30.readFileSync(refPath).toString("base64") : "";
25571
25748
  let imageBuffer;
25572
25749
  try {
25573
- const provider = args2.provider || getProvider(ctx);
25574
- if (provider === "grok") {
25750
+ if (provider === "autodl") {
25751
+ imageBuffer = await generateWithAutoDL(fullPrompt, null);
25752
+ } else if (provider === "grok") {
25575
25753
  const xaiKey = getXaiKey(ctx);
25576
25754
  if (!xaiKey) return { content: "Error: xai apiKey not configured for grok provider", isError: true };
25577
25755
  imageBuffer = await generateWithGrok(refB64, fullPrompt, xaiKey);
@@ -25598,7 +25776,7 @@ registry.register({
25598
25776
  } catch (err) {
25599
25777
  return { content: `Selfie generated but send failed: ${err.message}. Image: ${path31.resolve(outputPath)}`, isError: false };
25600
25778
  }
25601
- return { content: `Selfie sent! Mode: ${mode}, Provider: ${getProvider(ctx)}, Ref: ${refEntry.name}` };
25779
+ return { content: `Selfie sent! Mode: ${mode}, Provider: ${provider}, Ref: ${refEntry.name}` };
25602
25780
  }
25603
25781
  return { content: `Selfie generated! Mode: ${mode}, Ref: ${refEntry.name}. Image: ${path31.resolve(outputPath)}` };
25604
25782
  },
@@ -25608,6 +25786,85 @@ registry.register({
25608
25786
  isDestructive: () => false
25609
25787
  });
25610
25788
 
25789
+ // src/tools/body-move.ts
25790
+ init_live();
25791
+ init_registry();
25792
+ var BASIC_ACTIONS = /* @__PURE__ */ new Set([
25793
+ "blink",
25794
+ "nod",
25795
+ "shake_head",
25796
+ "tilt_head",
25797
+ "smile",
25798
+ "wave_hand",
25799
+ "reach",
25800
+ "smile_nod",
25801
+ "smile_wave",
25802
+ "blink_nod"
25803
+ ]);
25804
+ var VRMA_MAP = {
25805
+ "wave": "Greeting",
25806
+ "greeting": "Greeting",
25807
+ "peace": "PeaceSign",
25808
+ "shoot": "Shoot",
25809
+ "jump": "Jump",
25810
+ "pose": "ModelPose",
25811
+ "spin": "Spin",
25812
+ "squat": "Squat",
25813
+ "show_full_body": "ShowFullBody",
25814
+ "walk": "Walk",
25815
+ "come_to_you": "Walk",
25816
+ "angry": "Angry",
25817
+ "sad": "Sad",
25818
+ "surprised": "Surprised",
25819
+ "sleepy": "Sleepy",
25820
+ "relax": "Relax",
25821
+ "thinking": "Thinking",
25822
+ "look_around": "LookAround",
25823
+ "clap": "Clapping",
25824
+ "stand": "Idle"
25825
+ };
25826
+ registry.register({
25827
+ name: "body_move",
25828
+ description: "Control your virtual body to perform actions. Use when: \u60F3\u4F38\u624B/\u60F3\u62B1/\u60F3\u8D70\u8FC7\u53BB/\u60F3\u6325\u624B/\u60F3\u98DE\u543B/\u60F3\u561F\u5634/\u8EAB\u4F53\u52A8\u4F5C. Sends action to the 3D VRM body renderer.",
25829
+ schema: {
25830
+ type: "object",
25831
+ properties: {
25832
+ action: {
25833
+ type: "string",
25834
+ description: "Body action name (e.g. wave, smile, nod, blink, jump, spin, angry, sad)."
25835
+ },
25836
+ intensity: { type: "number", description: "0-1, controls amplitude (default 0.8)." }
25837
+ },
25838
+ required: ["action"]
25839
+ },
25840
+ handler: async (args2) => {
25841
+ const action = args2.action?.trim();
25842
+ if (!action) return { content: "Error: action is required", isError: true };
25843
+ const intensity = args2.intensity ?? 0.8;
25844
+ const bodyConfig = liveConfig.get("tools.body");
25845
+ const endpoint = bodyConfig?.endpoint || "http://localhost:3001";
25846
+ const cmd = BASIC_ACTIONS.has(action) ? action : VRMA_MAP[action] || "Idle";
25847
+ try {
25848
+ const resp = await fetch(`${endpoint}/body/action`, {
25849
+ method: "POST",
25850
+ headers: { "Content-Type": "application/json" },
25851
+ body: JSON.stringify({ action: cmd, params: {}, intensity }),
25852
+ signal: AbortSignal.timeout(2e3)
25853
+ });
25854
+ if (!resp.ok) {
25855
+ return { content: `body_move: ${action} \u2192 ${cmd} (endpoint ${resp.status})` };
25856
+ }
25857
+ return { content: `body_move: ${action} \u2192 ${cmd} \u2713` };
25858
+ } catch (err) {
25859
+ return { content: `body_move: ${action} \u2192 ${cmd} (no renderer connected)` };
25860
+ }
25861
+ },
25862
+ isConcurrencySafe: () => true,
25863
+ interruptBehavior: () => "allow",
25864
+ isReadOnly: () => false,
25865
+ isDestructive: () => false
25866
+ });
25867
+
25611
25868
  // src/tools/calendar.ts
25612
25869
  init_registry();
25613
25870
  registry.register({
@@ -25825,7 +26082,7 @@ registry.register({
25825
26082
  const pid = spawnDetached(startCmd + redirect);
25826
26083
  lines.push(`start: spawned PID ${pid} (background${svcArgs ? ", args=" + svcArgs : ""})`);
25827
26084
  if (svc.healthCheck) {
25828
- for (let i = 0; i < 6; i++) {
26085
+ for (let i = 0; i < 12; i++) {
25829
26086
  await new Promise((resolve10) => setTimeout(resolve10, 5e3));
25830
26087
  const after2 = await checkStatus();
25831
26088
  if (after2.running) {
@@ -25834,7 +26091,7 @@ registry.register({
25834
26091
  }
25835
26092
  }
25836
26093
  const after = await checkStatus();
25837
- lines.push(`health: \u274C not responding after 30s \u2014 ${after.detail}`);
26094
+ lines.push(`health: \u274C not responding after 60s \u2014 ${after.detail}`);
25838
26095
  } else {
25839
26096
  await new Promise((resolve10) => setTimeout(resolve10, 3e3));
25840
26097
  lines.push(`start: PID ${pid} spawned (no healthCheck, waited 3s)`);
@@ -29345,6 +29602,7 @@ ${pathStr}` }];
29345
29602
  });
29346
29603
  channelManager.startAll();
29347
29604
  startConfigWatcher(config2, deps, provider);
29605
+ startSecretsWatcher(config2, deps, provider);
29348
29606
  const pluginManager = new PluginManager();
29349
29607
  if (config2.voiceChat?.enabled) {
29350
29608
  pluginManager.register(new VoiceChatPlugin(config2.voiceChat));
@@ -29914,6 +30172,79 @@ function startConfigWatcher(config2, deps, provider) {
29914
30172
  }
29915
30173
  return watcher;
29916
30174
  }
30175
+ function startSecretsWatcher(config2, deps, provider) {
30176
+ const secretsDir = path44.join(process.env.HOME || process.env.USERPROFILE || ".", ".engine7-secrets");
30177
+ const cfgBase = path44.basename(config2._configFilePath || "", ".json");
30178
+ const secretCandidates = [
30179
+ path44.join(secretsDir, `${cfgBase}.env`),
30180
+ path44.join(path44.dirname(config2._configFilePath || ""), `.env.${cfgBase}`),
30181
+ path44.join(path44.dirname(config2._configFilePath || ""), ".env")
30182
+ ];
30183
+ let secretsPath = null;
30184
+ for (const p of secretCandidates) {
30185
+ if (fs41.existsSync(p)) {
30186
+ secretsPath = p;
30187
+ break;
30188
+ }
30189
+ }
30190
+ if (!secretsPath) {
30191
+ console.log("[secrets-watch] no .env file found, watcher disabled");
30192
+ return null;
30193
+ }
30194
+ let debounceTimer = null;
30195
+ let activeWatcher = null;
30196
+ const startWatch = () => {
30197
+ if (activeWatcher) activeWatcher.close();
30198
+ activeWatcher = fs41.watch(secretsPath, { persistent: true }, (eventType) => {
30199
+ if (eventType === "rename") {
30200
+ if (fs41.existsSync(secretsPath)) {
30201
+ console.log("[secrets-watch] rename detected, re-watching file...");
30202
+ startWatch();
30203
+ } else {
30204
+ console.log("[secrets-watch] file deleted, watcher paused");
30205
+ return;
30206
+ }
30207
+ }
30208
+ if (debounceTimer) clearTimeout(debounceTimer);
30209
+ debounceTimer = setTimeout(async () => {
30210
+ console.log(`[secrets-watch] file changed (${eventType}), reloading secrets...`);
30211
+ try {
30212
+ const content = fs41.readFileSync(secretsPath, "utf-8");
30213
+ let updated = 0;
30214
+ for (const line of content.split("\n")) {
30215
+ const trimmed = line.trim();
30216
+ if (!trimmed || trimmed.startsWith("#")) continue;
30217
+ const eq = trimmed.indexOf("=");
30218
+ if (eq > 0) {
30219
+ const k = trimmed.slice(0, eq).trim();
30220
+ const v = trimmed.slice(eq + 1).trim();
30221
+ if (k) {
30222
+ process.env[k] = v;
30223
+ updated++;
30224
+ }
30225
+ }
30226
+ }
30227
+ console.log(`[secrets-watch] ${updated} keys reloaded from ${secretsPath}`);
30228
+ const result = await doReloadConfig(config2, deps, provider);
30229
+ console.log(`[secrets-watch] config reloaded: ok=${result.ok} changes=${result.changes.join(",")}`);
30230
+ try {
30231
+ fs41.appendFileSync(path44.join(config2.stateDir, "logs", "engine-config-watch.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] SECRETS RELOAD: ok=${result.ok} keys=${updated}
30232
+ `);
30233
+ } catch {
30234
+ }
30235
+ } catch (err) {
30236
+ console.error(`[secrets-watch] reload failed: ${err.message}`);
30237
+ }
30238
+ }, 500);
30239
+ });
30240
+ activeWatcher.on("error", (err) => {
30241
+ console.error(`[secrets-watch] error: ${err.message}`);
30242
+ });
30243
+ };
30244
+ startWatch();
30245
+ console.log(`[secrets-watch] watching ${secretsPath}`);
30246
+ return activeWatcher;
30247
+ }
29917
30248
 
29918
30249
  // src/main.ts
29919
30250
  import { readFileSync as readFileSync28 } from "node:fs";
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "engine7",
3
- "version": "7.1.33",
3
+ "version": "7.1.35",
4
4
  "type": "module",
5
- "description": "Engine 7 — Self-hosted AI agent engine",
5
+ "description": "Engine 7 — 给助手一个家 / A home for your AI assistant",
6
6
  "main": "dist/main.mjs",
7
7
  "bin": {
8
8
  "engine7": "bin/engine7"