@aiyiran/myclaw 1.0.209 → 1.0.211

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.
@@ -49,7 +49,7 @@
49
49
 
50
50
  // ═══ 构建预览 URL ═══
51
51
  function buildPreviewUrl(data, assetPath) {
52
- var wsName = data.workspace_id || '';
52
+ var wsName = data.workspace_id || 'main';
53
53
  var wsPrefix = wsName === 'main' ? 'workspace' : 'workspace-' + wsName;
54
54
  return window.location.origin + '/cmd/api/preview?path=' + wsPrefix + '/' + assetPath;
55
55
  }
@@ -137,13 +137,25 @@
137
137
  ].join(';');
138
138
  header.innerHTML = '<span>\uD83C\uDFA8 \u5B66\u751F\u4F5C\u54C1</span>';
139
139
 
140
+ var headerRight = document.createElement('span');
141
+ headerRight.style.cssText = 'display:flex;align-items:center;gap:10px;';
142
+
143
+ var publishBtn = document.createElement('span');
144
+ publishBtn.textContent = '\u53D1\u5E03';
145
+ publishBtn.style.cssText = 'cursor:pointer;padding:2px 10px;border-radius:3px;font-size:12px;background:rgba(255,255,255,0.08);transition:background 0.15s;';
146
+ publishBtn.onmouseenter = function () { publishBtn.style.background = 'rgba(255,255,255,0.18)'; };
147
+ publishBtn.onmouseleave = function () { publishBtn.style.background = 'rgba(255,255,255,0.08)'; };
148
+ publishBtn.onclick = function () { openPublishModal(); };
149
+ headerRight.appendChild(publishBtn);
150
+
140
151
  var closeBtn = document.createElement('span');
141
152
  closeBtn.textContent = '\u2715';
142
153
  closeBtn.style.cssText = 'cursor:pointer;padding:2px 6px;border-radius:3px;font-size:14px;transition:background 0.15s;';
143
154
  closeBtn.onmouseenter = function () { closeBtn.style.background = 'rgba(255,255,255,0.1)'; };
144
155
  closeBtn.onmouseleave = function () { closeBtn.style.background = 'none'; };
145
156
  closeBtn.onclick = function () { closeArtifactsPanel(); };
146
- header.appendChild(closeBtn);
157
+ headerRight.appendChild(closeBtn);
158
+ header.appendChild(headerRight);
147
159
 
148
160
  // 内容区
149
161
  var content = document.createElement('div');
@@ -179,7 +191,7 @@
179
191
 
180
192
  // ═══ 请求数据 ═══
181
193
  function getArtifactsUrl() {
182
- var agentName = getAgentName();
194
+ var agentName = getAgentName() || 'main';
183
195
  var wsPrefix = agentName === 'main' ? 'workspace' : 'workspace-' + agentName;
184
196
  return window.location.origin + '/cmd/api/preview?path=' + wsPrefix + '/.myclaw/__MY_ARTIFACTS__.json';
185
197
  }
@@ -235,13 +247,32 @@
235
247
  ].join(';');
236
248
  tableHeader.innerHTML = [
237
249
  '<span style="width:30px;text-align:center;">#</span>',
238
- '<span style="flex:2;">标题</span>',
239
- '<span style="flex:1.5;">文件名</span>',
250
+ '<span style="flex:2;">文件名</span>',
251
+ '<span style="flex:1;text-align:center;">类别</span>',
240
252
  '<span style="flex:1;text-align:right;">更新时间</span>',
241
253
  ].join('');
242
254
  container.appendChild(tableHeader);
243
255
 
244
- data.assets.forEach(function (asset, idx) {
256
+ // updated_at 从新到旧排序
257
+ var sorted = data.assets.slice().sort(function (a, b) {
258
+ var ta = a.updated_at || '';
259
+ var tb = b.updated_at || '';
260
+ return tb.localeCompare(ta);
261
+ });
262
+
263
+ var topUpdatedAt = data.updated_at || '';
264
+
265
+ sorted.forEach(function (asset, idx) {
266
+ // 判断是否为最新(asset 的 updated_at 与顶层一致)
267
+ var isLatest = topUpdatedAt && asset.updated_at === topUpdatedAt;
268
+ var latestSeenKey = 'myclaw-artifacts-latest-seen-' + (asset.id || asset.path);
269
+ var isLatestSeen = localStorage.getItem(latestSeenKey);
270
+
271
+ // 判断是否有更新(created_at !== updated_at)
272
+ var isUpdated = asset.created_at && asset.updated_at && asset.created_at !== asset.updated_at;
273
+ var updateSeenKey = 'myclaw-artifacts-update-seen-' + (asset.id || asset.path);
274
+ var isUpdateSeen = localStorage.getItem(updateSeenKey);
275
+
245
276
  var row = document.createElement('div');
246
277
  row.style.cssText = [
247
278
  'display: flex',
@@ -253,31 +284,47 @@
253
284
  ].join(';');
254
285
  row.onmouseenter = function () { row.style.background = '#2d2d3f'; };
255
286
  row.onmouseleave = function () { row.style.background = 'transparent'; };
256
- row.onclick = function () { openPreviewModal(data, asset); };
287
+ row.onclick = function () {
288
+ if (isLatest && !isLatestSeen) localStorage.setItem(latestSeenKey, '1');
289
+ if (isUpdated && !isUpdateSeen) localStorage.setItem(updateSeenKey, '1');
290
+ openPreviewModal(data, asset);
291
+ };
257
292
 
258
293
  // 编号
259
294
  var num = document.createElement('span');
260
295
  num.style.cssText = 'width:30px;text-align:center;color:#888;flex-shrink:0;';
261
296
  num.textContent = String(idx + 1);
262
297
 
263
- // 标题
264
- var title = document.createElement('span');
265
- title.style.cssText = 'flex:2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
266
- title.textContent = asset.name || '未命名';
267
- title.title = asset.name || '';
268
-
269
- // 文件名
298
+ // 文件名(从 path 提取)+ 标记
270
299
  var fname = document.createElement('span');
271
- fname.style.cssText = 'flex:1.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#888;';
272
- // 从 path 中提取文件名
300
+ fname.style.cssText = 'flex:2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:flex;align-items:center;gap:6px;';
273
301
  var pathParts = (asset.path || '').split('/');
274
- fname.textContent = pathParts[pathParts.length - 1] || '';
275
- fname.title = asset.path || '';
302
+ var nameSpan = document.createElement('span');
303
+ nameSpan.textContent = pathParts[pathParts.length - 1] || asset.name || '未命名';
304
+ nameSpan.title = asset.path || '';
305
+ fname.appendChild(nameSpan);
306
+
307
+ // [最新] 优先于 [有更新]
308
+ if (isLatest && !isLatestSeen) {
309
+ var badge = document.createElement('span');
310
+ badge.style.cssText = 'color:#ff4444;font-size:10px;font-weight:bold;flex-shrink:0;';
311
+ badge.textContent = '[最新]';
312
+ fname.appendChild(badge);
313
+ } else if (isUpdated && !isUpdateSeen) {
314
+ var badge = document.createElement('span');
315
+ badge.style.cssText = 'color:#10b981;font-size:10px;font-weight:bold;flex-shrink:0;';
316
+ badge.textContent = '[有更新]';
317
+ fname.appendChild(badge);
318
+ }
319
+
320
+ // 类别
321
+ var type = document.createElement('span');
322
+ type.style.cssText = 'flex:1;text-align:center;color:#888;';
323
+ type.textContent = asset.type ? asset.type.toUpperCase() : '';
276
324
 
277
325
  // 更新时间
278
326
  var time = document.createElement('span');
279
327
  time.style.cssText = 'flex:1;text-align:right;color:#888;font-size:11px;flex-shrink:0;';
280
- // 优先用 asset 自身的,fallback 到 data 级别的 updated_at
281
328
  var updatedAt = asset.updated_at || data.updated_at || '';
282
329
  if (updatedAt) {
283
330
  try {
@@ -289,8 +336,8 @@
289
336
  }
290
337
 
291
338
  row.appendChild(num);
292
- row.appendChild(title);
293
339
  row.appendChild(fname);
340
+ row.appendChild(type);
294
341
  row.appendChild(time);
295
342
  container.appendChild(row);
296
343
  });
@@ -318,8 +365,8 @@
318
365
 
319
366
  var box = document.createElement('div');
320
367
  box.style.cssText = [
321
- 'width: 90vw',
322
- 'height: 85vh',
368
+ 'width: 98vw',
369
+ 'height: 96vh',
323
370
  'background: #1e1e2e',
324
371
  'border-radius: 8px',
325
372
  'overflow: hidden',
@@ -346,8 +393,8 @@
346
393
 
347
394
  var closeBtn = document.createElement('span');
348
395
  closeBtn.textContent = '\u2715';
349
- closeBtn.style.cssText = 'cursor:pointer;padding:2px 6px;border-radius:3px;font-size:14px;transition:background 0.15s;';
350
- closeBtn.onmouseenter = function () { closeBtn.style.background = 'rgba(255,255,255,0.1)'; };
396
+ closeBtn.style.cssText = 'cursor:pointer;padding:4px 10px;border-radius:4px;font-size:18px;font-weight:bold;transition:background 0.15s;';
397
+ closeBtn.onmouseenter = function () { closeBtn.style.background = 'rgba(255,255,255,0.15)'; };
351
398
  closeBtn.onmouseleave = function () { closeBtn.style.background = 'none'; };
352
399
  closeBtn.onclick = function () { closePreviewModal(); };
353
400
  header.appendChild(closeBtn);
@@ -366,11 +413,6 @@
366
413
  box.appendChild(iframe);
367
414
  overlay.appendChild(box);
368
415
 
369
- // 点击遮罩关闭
370
- overlay.onclick = function (e) {
371
- if (e.target === overlay) closePreviewModal();
372
- };
373
-
374
416
  document.body.appendChild(overlay);
375
417
  }
376
418
 
@@ -379,6 +421,182 @@
379
421
  if (modal) modal.remove();
380
422
  }
381
423
 
424
+ // ═══ 发布弹框 ═══
425
+ function openPublishModal() {
426
+ if (document.querySelector('#myclaw-artifacts-publish-modal')) return;
427
+ if (!cachedData) return;
428
+
429
+ var overlay = document.createElement('div');
430
+ overlay.id = 'myclaw-artifacts-publish-modal';
431
+ overlay.style.cssText = [
432
+ 'position: fixed',
433
+ 'top: 0',
434
+ 'left: 0',
435
+ 'width: 100vw',
436
+ 'height: 100vh',
437
+ 'background: rgba(0, 0, 0, 0.4)',
438
+ 'z-index: 99999',
439
+ 'display: flex',
440
+ 'align-items: center',
441
+ 'justify-content: center',
442
+ 'animation: myclaw-fade-in 0.15s ease',
443
+ ].join(';');
444
+
445
+ var box = document.createElement('div');
446
+ box.style.cssText = [
447
+ 'width: 500px',
448
+ 'background: #1e1e2e',
449
+ 'border-radius: 8px',
450
+ 'overflow: hidden',
451
+ 'display: flex',
452
+ 'flex-direction: column',
453
+ 'box-shadow: 0 8px 32px rgba(0,0,0,0.5)',
454
+ ].join(';');
455
+
456
+ // 标题栏
457
+ var header = document.createElement('div');
458
+ header.style.cssText = [
459
+ 'display: flex',
460
+ 'align-items: center',
461
+ 'justify-content: space-between',
462
+ 'padding: 10px 14px',
463
+ 'background: #2d2d3f',
464
+ 'color: #cdd6f4',
465
+ 'font-size: 13px',
466
+ 'font-family: monospace',
467
+ 'user-select: none',
468
+ 'flex-shrink: 0',
469
+ ].join(';');
470
+ header.innerHTML = '<span>\uD83D\uDE80 \u53D1\u5E03\u4F5C\u54C1</span>';
471
+
472
+ var closeBtn = document.createElement('span');
473
+ closeBtn.textContent = '\u2715';
474
+ closeBtn.style.cssText = 'cursor:pointer;padding:2px 6px;border-radius:3px;font-size:14px;transition:background 0.15s;';
475
+ closeBtn.onmouseenter = function () { closeBtn.style.background = 'rgba(255,255,255,0.1)'; };
476
+ closeBtn.onmouseleave = function () { closeBtn.style.background = 'none'; };
477
+ closeBtn.onclick = function () { closePublishModal(); };
478
+ header.appendChild(closeBtn);
479
+
480
+ // 表单内容
481
+ var form = document.createElement('div');
482
+ form.style.cssText = 'padding: 20px;display:flex;flex-direction:column;gap:16px;color:#cdd6f4;font-family:monospace;font-size:13px;';
483
+
484
+ // 字段 0:作品名称
485
+ var titleGroup = document.createElement('div');
486
+ titleGroup.style.cssText = 'display:flex;flex-direction:column;gap:6px;';
487
+ var titleLabel = document.createElement('label');
488
+ titleLabel.textContent = '\u4F5C\u54C1\u540D\u79F0';
489
+ titleLabel.style.cssText = 'font-size:12px;color:#888;';
490
+ var titleInput = document.createElement('input');
491
+ titleInput.type = 'text';
492
+ titleInput.placeholder = '\u8F93\u5165\u5C55\u793A\u6807\u9898';
493
+ titleInput.value = cachedData.title || '';
494
+ titleInput.style.cssText = 'padding:8px 10px;background:#252536;border:1px solid #3d3d5c;border-radius:4px;color:#cdd6f4;font-size:13px;font-family:monospace;outline:none;';
495
+ titleInput.onfocus = function () { titleInput.style.borderColor = '#6c6caa'; };
496
+ titleInput.onblur = function () { titleInput.style.borderColor = '#3d3d5c'; };
497
+ titleGroup.appendChild(titleLabel);
498
+ titleGroup.appendChild(titleInput);
499
+ form.appendChild(titleGroup);
500
+
501
+ // 字段 1:选择封面图片
502
+ var coverGroup = document.createElement('div');
503
+ coverGroup.style.cssText = 'display:flex;flex-direction:column;gap:6px;';
504
+ var coverLabel = document.createElement('label');
505
+ coverLabel.textContent = '\u9009\u62E9\u5C01\u9762\u56FE\u7247';
506
+ coverLabel.style.cssText = 'font-size:12px;color:#888;';
507
+ var coverRow = document.createElement('div');
508
+ coverRow.style.cssText = 'display:flex;align-items:center;gap:10px;';
509
+ var coverSelect = document.createElement('select');
510
+ coverSelect.style.cssText = 'flex:1;padding:8px 10px;background:#252536;border:1px solid #3d3d5c;border-radius:4px;color:#cdd6f4;font-size:13px;font-family:monospace;outline:none;';
511
+ coverSelect.onfocus = function () { coverSelect.style.borderColor = '#6c6caa'; };
512
+ coverSelect.onblur = function () { coverSelect.style.borderColor = '#3d3d5c'; };
513
+ var coverDefaultOpt = document.createElement('option');
514
+ coverDefaultOpt.value = '';
515
+ coverDefaultOpt.textContent = '\u4E0D\u9009\u62E9';
516
+ coverSelect.appendChild(coverDefaultOpt);
517
+ var imageAssets = (cachedData.assets || []).filter(function (a) { return a.type === 'image'; });
518
+ imageAssets.forEach(function (asset) {
519
+ var opt = document.createElement('option');
520
+ opt.value = asset.path;
521
+ opt.textContent = asset.name;
522
+ coverSelect.appendChild(opt);
523
+ });
524
+ var coverPreview = document.createElement('img');
525
+ coverPreview.style.cssText = 'width:60px;height:40px;object-fit:cover;border-radius:4px;border:1px solid #3d3d5c;display:none;background:#252536;';
526
+ coverSelect.onchange = function () {
527
+ if (coverSelect.value) {
528
+ coverPreview.src = buildPreviewUrl(cachedData, coverSelect.value);
529
+ coverPreview.style.display = 'block';
530
+ } else {
531
+ coverPreview.src = '';
532
+ coverPreview.style.display = 'none';
533
+ }
534
+ };
535
+ coverRow.appendChild(coverSelect);
536
+ coverRow.appendChild(coverPreview);
537
+ coverGroup.appendChild(coverLabel);
538
+ coverGroup.appendChild(coverRow);
539
+ form.appendChild(coverGroup);
540
+
541
+ // 字段 2:入口文件
542
+ var entryGroup = document.createElement('div');
543
+ entryGroup.style.cssText = 'display:flex;flex-direction:column;gap:6px;';
544
+ var entryLabel = document.createElement('label');
545
+ entryLabel.textContent = '\u5165\u53E3\u6587\u4EF6';
546
+ entryLabel.style.cssText = 'font-size:12px;color:#888;';
547
+ var entrySelect = document.createElement('select');
548
+ entrySelect.style.cssText = 'padding:8px 10px;background:#252536;border:1px solid #3d3d5c;border-radius:4px;color:#cdd6f4;font-size:13px;font-family:monospace;outline:none;';
549
+ entrySelect.onfocus = function () { entrySelect.style.borderColor = '#6c6caa'; };
550
+ entrySelect.onblur = function () { entrySelect.style.borderColor = '#3d3d5c'; };
551
+ var entryDefaultOpt = document.createElement('option');
552
+ entryDefaultOpt.value = '';
553
+ entryDefaultOpt.textContent = '\u4E0D\u9009\u62E9';
554
+ entrySelect.appendChild(entryDefaultOpt);
555
+ var htmlAssets = (cachedData.assets || []).filter(function (a) { return a.type === 'html'; });
556
+ htmlAssets.forEach(function (asset) {
557
+ var opt = document.createElement('option');
558
+ opt.value = asset.path;
559
+ opt.textContent = asset.name;
560
+ entrySelect.appendChild(opt);
561
+ });
562
+ entryGroup.appendChild(entryLabel);
563
+ entryGroup.appendChild(entrySelect);
564
+ form.appendChild(entryGroup);
565
+
566
+ // 确认发布按钮
567
+ var submitBtn = document.createElement('button');
568
+ submitBtn.textContent = '\u786E\u8BA4\u53D1\u5E03';
569
+ submitBtn.style.cssText = 'margin:0 20px 20px;padding:10px;background:#4a4a7a;border:none;border-radius:4px;color:#cdd6f4;font-size:13px;font-family:monospace;cursor:pointer;transition:background 0.15s;';
570
+ submitBtn.onmouseenter = function () { submitBtn.style.background = '#5a5a9a'; };
571
+ submitBtn.onmouseleave = function () { submitBtn.style.background = '#4a4a7a'; };
572
+ submitBtn.onclick = function () {
573
+ var titleVal = titleInput.value.trim();
574
+ if (!titleVal) {
575
+ titleInput.style.borderColor = '#ff4444';
576
+ return;
577
+ }
578
+ var publishData = {
579
+ title: titleVal,
580
+ cover_path: coverSelect.value || '',
581
+ entry_path: entrySelect.value || '',
582
+ published_at: new Date().toISOString(),
583
+ };
584
+ console.log('[myclaw-artifacts-publish]', JSON.stringify(publishData));
585
+ closePublishModal();
586
+ };
587
+
588
+ box.appendChild(header);
589
+ box.appendChild(form);
590
+ box.appendChild(submitBtn);
591
+ overlay.appendChild(box);
592
+ document.body.appendChild(overlay);
593
+ }
594
+
595
+ function closePublishModal() {
596
+ var modal = document.querySelector('#myclaw-artifacts-publish-modal');
597
+ if (modal) modal.remove();
598
+ }
599
+
382
600
  // ═══ 注入样式 ═══
383
601
  function injectStyles() {
384
602
  if (document.querySelector('#myclaw-artifacts-styles')) return;
package/index.js CHANGED
@@ -396,6 +396,142 @@ function runPrepare() {
396
396
  });
397
397
  }
398
398
 
399
+ // ============================================================================
400
+ // TUI 唤起新对话上下文
401
+ // ============================================================================
402
+
403
+ // 预置的特殊 session ID 字典(按顺序使用)
404
+ const TUI_SESSION_SUFFIXES = [
405
+ 'helper', 'AAA', 'GO', '007', 'VIP', '666',
406
+ 'China', 'PRO', 'MAX', 'TOP', 'BEST',
407
+ 'GO', 'OK', 'YES', 'WIN', 'FUN'
408
+ ];
409
+
410
+ // Session 索引存储文件
411
+ const SESSION_INDEX_FILE = path.join(os.homedir(), '.openclaw', 'myclaw-session-index.json');
412
+
413
+ /**
414
+ * 获取 agent 的下一个 session 序号索引
415
+ */
416
+ function getNextSessionIndex(agentName) {
417
+ const fs = require('fs');
418
+ let indexData = {};
419
+
420
+ // 读取现有索引
421
+ if (fs.existsSync(SESSION_INDEX_FILE)) {
422
+ try {
423
+ const raw = fs.readFileSync(SESSION_INDEX_FILE, 'utf8');
424
+ indexData = JSON.parse(raw);
425
+ } catch (err) {
426
+ console.warn('[' + colors.yellow + '警告' + colors.nc + '] 读取 session 索引失败,将重置: ' + err.message);
427
+ }
428
+ }
429
+
430
+ // 获取当前 agent 的索引,不存在则从 0 开始
431
+ const currentIndex = indexData[agentName] || 0;
432
+
433
+ // 计算下一个索引(循环使用)
434
+ const nextIndex = currentIndex % TUI_SESSION_SUFFIXES.length;
435
+
436
+ // 更新索引数据
437
+ indexData[agentName] = currentIndex + 1;
438
+
439
+ // 确保目录存在
440
+ const dir = path.dirname(SESSION_INDEX_FILE);
441
+ if (!fs.existsSync(dir)) {
442
+ fs.mkdirSync(dir, { recursive: true });
443
+ }
444
+
445
+ // 写回文件
446
+ try {
447
+ fs.writeFileSync(SESSION_INDEX_FILE, JSON.stringify(indexData, null, 2) + '\n', 'utf8');
448
+ } catch (err) {
449
+ console.warn('[' + colors.yellow + '警告' + colors.nc + '] 保存 session 索引失败: ' + err.message);
450
+ }
451
+
452
+ return nextIndex;
453
+ }
454
+
455
+ /**
456
+ * 获取 agent 的下一个 session 后缀
457
+ */
458
+ function getNextSessionSuffix(agentName) {
459
+ const index = getNextSessionIndex(agentName);
460
+ return TUI_SESSION_SUFFIXES[index];
461
+ }
462
+
463
+ function runTui(interactive = false) {
464
+ const { spawn } = require('child_process');
465
+
466
+ // 获取 agent 名称
467
+ let agentName;
468
+ if (interactive) {
469
+ // 交互模式:需要询问用户
470
+ return askAgentNameForTui();
471
+ } else {
472
+ // 命令行模式:从参数获取,默认 'main'
473
+ agentName = args[1] || 'main';
474
+ doTuiCommand(agentName);
475
+ }
476
+ }
477
+
478
+ // 交互式询问 Agent 名称
479
+ function askAgentNameForTui() {
480
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
481
+ rl.question(colors.cyan + '请输入 Agent 名称: ' + colors.nc, function (answer) {
482
+ rl.close();
483
+ const agentName = (answer || '').trim();
484
+ if (!agentName) {
485
+ console.log(colors.red + 'Agent 名称不能为空。' + colors.nc);
486
+ console.log('');
487
+ askAgentNameForTui();
488
+ return;
489
+ }
490
+ doTuiCommand(agentName);
491
+ });
492
+ }
493
+
494
+ // 执行 TUI 命令
495
+ function doTuiCommand(agentName) {
496
+ const { spawn } = require('child_process');
497
+
498
+ // 获取当前 agent 的下一个 session 后缀
499
+ const sessionSuffix = getNextSessionSuffix(agentName);
500
+ const sessionKey = 'agent:' + agentName + ':' + sessionSuffix;
501
+
502
+ // 默认消息
503
+ const messageParam = '你好';
504
+
505
+ console.log('[' + colors.blue + 'TUI' + colors.nc + '] 唤起新对话上下文');
506
+ console.log(' Agent: ' + colors.green + agentName + colors.nc);
507
+ console.log(' Session: ' + colors.cyan + sessionKey + colors.nc);
508
+ console.log(' Message: ' + colors.dim + messageParam + colors.nc);
509
+ console.log('');
510
+
511
+ // 发送消息给 agent
512
+ const openclawArgs = ['tui', '--session', sessionKey, '--message', messageParam];
513
+
514
+ // 打印命令时给消息加上引号(仅用于显示)
515
+ const displayArgs = ['tui', '--session', sessionKey, '--message', '"' + messageParam + '"'];
516
+ console.log('[' + colors.dim + '执行命令: openclaw ' + displayArgs.join(' ') + colors.nc + ']');
517
+ console.log('');
518
+
519
+ // 启动进程,保存引用以便稍后杀掉
520
+ const child = spawn('openclaw', openclawArgs, {
521
+ stdio: 'ignore',
522
+ shell: true
523
+ });
524
+
525
+ console.log('[' + colors.blue + 'TUI' + colors.nc + '] 消息已发送,等待对话生成...');
526
+
527
+ // 等待 10 秒后杀掉进程
528
+ setTimeout(() => {
529
+ child.kill();
530
+ console.log('[' + colors.green + '完成' + colors.nc + '] 对话上下文已唤起');
531
+ process.exit(0);
532
+ }, 10000);
533
+ }
534
+
399
535
  // ============================================================================
400
536
  // 创建 Windows 桌面快捷启动脚本
401
537
  // ============================================================================
@@ -411,7 +547,7 @@ function runBat() {
411
547
  }
412
548
 
413
549
  const myClawDir = path.join(process.env.LOCALAPPDATA || os.tmpdir(), 'myclaw');
414
- try { fs.mkdirSync(myClawDir, { recursive: true }); } catch {}
550
+ try { fs.mkdirSync(myClawDir, { recursive: true }); } catch { }
415
551
 
416
552
  const batPath = path.join(myClawDir, 'openclaw-launcher.bat');
417
553
  const desktopPath = path.join(os.homedir(), 'Desktop');
@@ -429,9 +565,9 @@ function runBat() {
429
565
  try {
430
566
  const oldLinks = fs.readdirSync(desktopPath).filter(f => f.endsWith('.lnk') && (f.includes('OpenClaw') || f.match(/^\d+\.\d+\.\d+_OpenClaw/)));
431
567
  for (const old of oldLinks) {
432
- try { fs.unlinkSync(path.join(desktopPath, old)); } catch {}
568
+ try { fs.unlinkSync(path.join(desktopPath, old)); } catch { }
433
569
  }
434
- } catch {}
570
+ } catch { }
435
571
 
436
572
  const lnkPath = path.join(desktopPath, ver + '_OpenClaw_' + mm + '-' + dd + '_' + hh + '-' + mi + '-' + ss + '.lnk');
437
573
 
@@ -461,7 +597,7 @@ pause >nul
461
597
  // 用 PowerShell 下载图标并创建带图标的桌面快捷方式 + 刷新桌面
462
598
  const iconPath = path.join(myClawDir, 'openclaw.ico');
463
599
  const iconUrl = 'https://cdn.yiranlaoshi.com/software/myclaw/openclaw.ico';
464
-
600
+
465
601
  const psFile = path.join(myClawDir, 'create-shortcuts.ps1');
466
602
  const psContent = [
467
603
  '$ErrorActionPreference = \'Continue\'',
@@ -580,7 +716,7 @@ function runOpen() {
580
716
  try {
581
717
  const releaseInfo = fs.readFileSync('/proc/version', 'utf8');
582
718
  if (/microsoft|wsl/i.test(releaseInfo)) isWSL = true;
583
- } catch {}
719
+ } catch { }
584
720
 
585
721
  if (isWSL) {
586
722
  // WSL2 环境下优先使用 Windows 端的 Chrome
@@ -612,7 +748,7 @@ function runOpen() {
612
748
  chromePath = p;
613
749
  break;
614
750
  }
615
- } catch {}
751
+ } catch { }
616
752
  }
617
753
 
618
754
  // 如果没有找到 Chrome
@@ -641,7 +777,7 @@ function runOpen() {
641
777
  }
642
778
  }
643
779
  }
644
- } catch {}
780
+ } catch { }
645
781
 
646
782
  console.log('安装 Chrome 后重新运行: ' + colors.yellow + 'myclaw open' + colors.nc);
647
783
  console.log('');
@@ -727,7 +863,7 @@ function runPatch() {
727
863
  current[parts[parts.length - 1]] = value;
728
864
  }
729
865
  const { config, configPath } = patchConfig(nested);
730
-
866
+
731
867
  // 【关键修复】深度合并不会删除错误的历史残留配置,这里主动帮学生清理掉遗留在顶层的 "exec"
732
868
  let requireSave = false;
733
869
  if (config && config.exec) {
@@ -735,7 +871,7 @@ function runPatch() {
735
871
  requireSave = true;
736
872
  console.log('[myclaw-config] ✅ 已清理历史遗留的错误顶层 "exec" 常驻配置');
737
873
  }
738
-
874
+
739
875
  if (requireSave) {
740
876
  const { writeConfig } = require('./find-config');
741
877
  writeConfig(config, configPath);
@@ -893,7 +1029,7 @@ function runUpdate() {
893
1029
  let currentVersion = 'unknown';
894
1030
  try {
895
1031
  currentVersion = require(path.join(__dirname, 'package.json')).version;
896
- } catch {}
1032
+ } catch { }
897
1033
 
898
1034
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
899
1035
  try {
@@ -942,7 +1078,7 @@ function runUpdate() {
942
1078
  console.log('');
943
1079
  console.log('[' + colors.yellow + '重试' + colors.nc + '] 8 秒后重试 (' + attempt + '/' + maxRetries + ')...');
944
1080
  const waitCmd = require('os').platform() === 'win32' ? 'timeout /t 8 >nul' : 'sleep 8';
945
- try { execSync(waitCmd, { stdio: 'ignore' }); } catch {}
1081
+ try { execSync(waitCmd, { stdio: 'ignore' }); } catch { }
946
1082
  } else {
947
1083
  console.log('');
948
1084
  console.log('[' + colors.red + '错误' + colors.nc + '] 升级失败: ' + err.message);
@@ -1037,31 +1173,38 @@ function padRight(str, len) {
1037
1173
  // ============================================================================
1038
1174
 
1039
1175
  const MENU_ITEMS = [
1040
- { key: 'start', label: '🦞启动🦞', cmd: 'mc start', desc: '把你的 AI 助手叫醒,让它开始工作', action: () => { const start = require('./start'); start.run(); } },
1041
- { key: 'restart', label: '重启', cmd: 'mc restart', desc: 'AI 助手卡住了?让它重新启动一下', action: runRestart },
1042
- { key: 'new', label: '😊新伙伴', cmd: 'mc new', desc: '创建一个新的 AI 助手,给它取个名字', action: runNew },
1043
- { key: 'status', label: '网址', cmd: 'mc status', desc: '获取控制台链接,复制到浏览器打开', action: runStatus },
1044
- { key: 'update', label: '升级', cmd: 'mc up', desc: '让 MyClaw 工具升级到最新版本', action: () => {
1045
- runUpdate();
1046
- console.log('');
1047
- console.log('🔧 自动执行 patch 以恢复自定义配置...');
1048
- runPatch();
1049
- if (detectPlatform() === 'windows') runBat();
1050
- }},
1051
- { key: 'patch', label: '修复', cmd: 'mc patch', desc: '给 AI 助手装上新技能和好看的外衣', action: runPatch },
1052
- { key: 'reinstall', label: '重装', cmd: 'mc longxia', desc: '出了大问题?把 AI 助手删了重新安装', action: runReinstall },
1053
- { key: 'uninstall', label: '卸载', cmd: 'mc uninstall', desc: '卸载 MyClaw,恢复 npm 源地址', action: runUninstall },
1054
- { key: 'wsl2reinstall', label: 'WSL2虚拟机重装', cmd: 'mc wsl2 --remote --force-phase1', desc: '强制从网络重新下载并重装 WSL2 虚拟机(仅限 Windows)', action: () => {
1055
- process.argv.push('--remote', '--force-phase1');
1056
- const wsl2 = require('./wsl2');
1057
- wsl2.run();
1058
- }},
1059
- { key: 'safe', label: '虚拟机屏蔽', cmd: 'mc safe', desc: '禁止 WSL2 访问 Windows 盘符(仅限 Windows)', action: () => {
1060
- const restrict = require('./restrict');
1061
- restrict.run();
1062
- }},
1176
+ { key: 'start', label: '🦞启动🦞', cmd: 'mc start', desc: '把你的 AI 助手叫醒,让它开始工作', action: () => { const start = require('./start'); start.run(); } },
1177
+ { key: 'restart', label: '重启', cmd: 'mc restart', desc: 'AI 助手卡住了?让它重新启动一下', action: runRestart },
1178
+ { key: 'new', label: '😊新伙伴', cmd: 'mc new', desc: '创建一个新的 AI 助手,给它取个名字', action: runNew },
1179
+ { key: 'tui', label: '新对话', cmd: 'mc tui', desc: '唤起新对话上下文', action: () => runTui(true) },
1180
+ { key: 'status', label: '网址', cmd: 'mc status', desc: '获取控制台链接,复制到浏览器打开', action: runStatus },
1181
+ {
1182
+ key: 'update', label: '升级', cmd: 'mc up', desc: '让 MyClaw 工具升级到最新版本', action: () => {
1183
+ runUpdate();
1184
+ console.log('');
1185
+ console.log('🔧 自动执行 patch 以恢复自定义配置...');
1186
+ runPatch();
1187
+ if (detectPlatform() === 'windows') runBat();
1188
+ }
1189
+ },
1190
+ { key: 'patch', label: '修复', cmd: 'mc patch', desc: ' AI 助手装上新技能和好看的外衣', action: runPatch },
1191
+ { key: 'reinstall', label: '重装', cmd: 'mc longxia', desc: '出了大问题?把 AI 助手删了重新安装', action: runReinstall },
1192
+ { key: 'uninstall', label: '卸载', cmd: 'mc uninstall', desc: '卸载 MyClaw,恢复 npm 源地址', action: runUninstall },
1193
+ {
1194
+ key: 'wsl2reinstall', label: 'WSL2虚拟机重装', cmd: 'mc wsl2 --remote --force-phase1', desc: '强制从网络重新下载并重装 WSL2 虚拟机(仅限 Windows)', action: () => {
1195
+ process.argv.push('--remote', '--force-phase1');
1196
+ const wsl2 = require('./wsl2');
1197
+ wsl2.run();
1198
+ }
1199
+ },
1200
+ {
1201
+ key: 'safe', label: '虚拟机屏蔽', cmd: 'mc safe', desc: '禁止 WSL2 访问 Windows 盘符(仅限 Windows)', action: () => {
1202
+ const restrict = require('./restrict');
1203
+ restrict.run();
1204
+ }
1205
+ },
1063
1206
  { key: 'bat', label: '桌面快捷方式', cmd: 'mc bat', desc: '在桌面生成一键启动脚本(仅限 Windows)', action: runBat },
1064
- { key: 'quit', label: '退出', cmd: 'Ctrl+C', desc: '不玩了,下次见', action: () => { console.log(' 👋 再见!\n'); process.exit(0); } },
1207
+ { key: 'quit', label: '退出', cmd: 'Ctrl+C', desc: '不玩了,下次见', action: () => { console.log(' 👋 再见!\n'); process.exit(0); } },
1065
1208
  ];
1066
1209
 
1067
1210
  function runInteractiveMenu() {
@@ -1124,6 +1267,13 @@ function runInteractiveMenu() {
1124
1267
  return;
1125
1268
  }
1126
1269
 
1270
+ // tui 命令需要交互式输入,不走菜单的"按回车返回"逻辑
1271
+ if (item.key === 'tui') {
1272
+ console.log('');
1273
+ item.action();
1274
+ return;
1275
+ }
1276
+
1127
1277
  console.log('');
1128
1278
  item.action();
1129
1279
 
@@ -1171,25 +1321,25 @@ function runInteractiveMenu() {
1171
1321
  // ============================================================================
1172
1322
 
1173
1323
  const INJECT_MENU = [
1174
- { key: '1', cmd: 'inject-minimax', desc: '注入 MiniMax 模型配置' },
1175
- { key: '2', cmd: 'inject-zai', desc: '注入智谱 GLM 模型配置' },
1176
- { key: '3', cmd: 'inject-image', desc: '注入图像生成模型配置 (vveai)' },
1177
- { key: '4', cmd: 'inject-search', desc: '注入 Tavily 搜索插件配置' },
1178
- { key: '5', cmd: 'inject-token', desc: '设置 Gateway Token 为 aiyiran' },
1324
+ { key: '1', cmd: 'inject-minimax', desc: '注入 MiniMax 模型配置' },
1325
+ { key: '2', cmd: 'inject-zai', desc: '注入智谱 GLM 模型配置' },
1326
+ { key: '3', cmd: 'inject-image', desc: '注入图像生成模型配置 (vveai)' },
1327
+ { key: '4', cmd: 'inject-search', desc: '注入 Tavily 搜索插件配置' },
1328
+ { key: '5', cmd: 'inject-token', desc: '设置 Gateway Token 为 aiyiran' },
1179
1329
  { key: '6', cmd: 'inject-workspaceAndSoul', desc: '替换默认 workspace 的 SOUL.md' },
1180
- { key: '7', cmd: 'inject-tooldeny', desc: 'deny image_generate + music_generate 内置工具' },
1181
- { key: 'a', cmd: 'all', desc: '执行以上全部注入' },
1330
+ { key: '7', cmd: 'inject-tooldeny', desc: 'deny image_generate + music_generate 内置工具' },
1331
+ { key: 'a', cmd: 'all', desc: '执行以上全部注入' },
1182
1332
  ];
1183
1333
 
1184
1334
  function runInjectCommand(cmd, extraArgs) {
1185
1335
  const modules = {
1186
- 'inject-minimax': './inject-minimax',
1187
- 'inject-zai': './inject-zai',
1188
- 'inject-image': './inject-image',
1189
- 'inject-search': './inject-search',
1190
- 'inject-token': './inject-token',
1336
+ 'inject-minimax': './inject-minimax',
1337
+ 'inject-zai': './inject-zai',
1338
+ 'inject-image': './inject-image',
1339
+ 'inject-search': './inject-search',
1340
+ 'inject-token': './inject-token',
1191
1341
  'inject-workspaceAndSoul': './inject-workspaceAndSoul',
1192
- 'inject-tooldeny': './inject-tooldeny',
1342
+ 'inject-tooldeny': './inject-tooldeny',
1193
1343
  };
1194
1344
  const mod = require(modules[cmd]);
1195
1345
  mod.run(extraArgs || []);
@@ -1267,6 +1417,7 @@ function showHelp() {
1267
1417
  console.log(' update 自动升级 MyClaw 到最新版本');
1268
1418
  console.log(' up 升级 + 刷新桌面快捷方式 (= update + bat)');
1269
1419
  console.log(' open 打开浏览器控制台(自动带 token)');
1420
+ console.log(' tui 唤起新对话上下文 (用法: mc tui <agentname>)');
1270
1421
  console.log(' fix 兜底修复(自动补装 WSL + Chrome,仅限 Windows)');
1271
1422
  console.log(' wsl2 WSL2 一键安装/修复 (仅限 Windows, 可选: --cli, --remote)');
1272
1423
  console.log(' safe 开启虚拟机屏蔽 (禁止自动挂载 Windows 盘符)');
@@ -1343,6 +1494,8 @@ if (!command) {
1343
1494
  runStart();
1344
1495
  } else if (command === 'open') {
1345
1496
  runOpen();
1497
+ } else if (command === 'tui') {
1498
+ runTui();
1346
1499
  } else if (command === 'fix') {
1347
1500
  runFix();
1348
1501
  } else if (command === 'wsl2') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiyiran/myclaw",
3
- "version": "1.0.209",
3
+ "version": "1.0.211",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
package/publish.sh CHANGED
@@ -1,19 +1,29 @@
1
1
  #!/bin/bash
2
2
  set -e # 遇到错误即停止
3
3
 
4
+ # 解析参数
5
+ GIT_MODE=false
6
+ for arg in "$@"; do
7
+ case $arg in
8
+ --git) GIT_MODE=true ;;
9
+ esac
10
+ done
11
+
4
12
  echo "📦 准备发布新版本..."
5
13
 
6
14
  # patch-manifest.json 由手动维护,不再自动构建
7
15
 
8
- # 1. 添加所有变动
9
- git add .
10
-
11
- # 2. 检查是否有未提交的代码,如果有则先提交
12
- if ! git diff-index --quiet HEAD; then
13
- echo "📝 发现有修改的内容,正在自动提交..."
14
- git commit -m "chore: auto update before publish"
15
- else
16
- echo "✅ 工作区很干净,不需要额外提交代码。"
16
+ if [ "$GIT_MODE" = true ]; then
17
+ # 1. 添加所有变动
18
+ git add .
19
+
20
+ # 2. 检查是否有未提交的代码,如果有则先提交
21
+ if ! git diff-index --quiet HEAD; then
22
+ echo "📝 发现有修改的内容,正在自动提交..."
23
+ git commit -m "chore: auto update before publish"
24
+ else
25
+ echo "✅ 工作区很干净,不需要额外提交代码。"
26
+ fi
17
27
  fi
18
28
 
19
29
  # 3. 自动更新最小版本号 (npm version patch 会自动再做一个改 package.json 的 commit 并且打上 Tag)
@@ -24,10 +34,12 @@ npm version patch
24
34
  echo "🚀 优先发布到 npm 仓库..."
25
35
  npm publish
26
36
 
27
- # 5. 推送代码和 Tag GitHub
28
- echo "☁️ 推送代码和版本 Tag 到 GitHub..."
29
- git push origin main || echo "⚠️ Git 推送失败 (网络问题),但不影响 npm 发布!请后续手动检查。"
30
- git push origin --tags || true
37
+ if [ "$GIT_MODE" = true ]; then
38
+ # 5. 推送代码和 Tag 到 GitHub
39
+ echo "☁️ 推送代码和版本 Tag GitHub..."
40
+ git push origin main || echo "⚠️ Git 推送失败 (网络问题),但不影响 npm 发布!请后续手动检查。"
41
+ git push origin --tags || true
42
+ fi
31
43
 
32
44
  VERSION=$(node -p "require('./package.json').version")
33
45
  echo "🎉 发布流执行完成!当前发布 NPM 版本号:v${VERSION}"
@@ -1,31 +1,30 @@
1
1
  ---
2
2
  name: yiran-skill-media
3
- description: 统一多媒体生成技能。支持图片和音乐生成,按资源类型自动路由到最优 provider,支持主备切换。生成文件保存在当前 workspace 的 media/ 目录。
3
+ description: 统一多媒体生成技能。支持图片和音乐生成,按资源类型自动路由到最优 provider,支持主备切换。资源生成规范:所有生成的资源必须存放在当前工作目录下,调用时通过 --output-dir 传入当前工作目录的绝对路径。
4
4
  ---
5
5
 
6
6
  # 统一多媒体生成
7
7
 
8
- ## 环境变量
8
+ ## 资源生成规范(必须遵守)
9
9
 
10
- | 变量 | 默认值 | 说明 |
11
- |------|--------|------|
12
- | `WORKSPACE_NAME` | `main` | workspace 名称 |
13
- | `OPENCLAW_HOME` | `~/.openclaw` | OpenClaw 安装目录(可选,用于覆盖默认路径) |
14
- | `VAPI_API_KEY` | - | VAPI 图片 API Key(备 provider) |
10
+ **`--output-dir` 为必填参数,必须传入绝对路径。**
15
11
 
16
- ## 输出路径规则
12
+ **规范:所有生成的资源文件必须存放在当前工作目录下。你在哪个目录工作,就把该目录的绝对路径传给 `--output-dir`。**
17
13
 
18
- - 生成文件保存在**当前 workspace 的 media/ 目录**
19
- - 文件名:`image_{时间戳}.png` / `music_{时间戳}.mp3`
14
+ 例如:
15
+ - 你当前在 `/root/.openclaw/workspace` → 传入 `--output-dir /root/.openclaw/workspace`
16
+ - 你当前在 `/root/.openclaw/workspace-dev` → 传入 `--output-dir /root/.openclaw/workspace-dev`
17
+
18
+ 这是硬性规范,不允许省略,不允许猜测路径。
20
19
 
21
20
  ## 一键脚本
22
21
 
23
22
  ```bash
24
- # 图片生成
25
- WORKSPACE_NAME=main ./image.sh "描述" [--aspect-ratio 16:9]
23
+ # 图片生成 — 传入当前工作目录的绝对路径
24
+ ./image.sh --output-dir "$(pwd)" "描述" [--aspect-ratio 16:9]
26
25
 
27
26
  # 音乐生成
28
- WORKSPACE_NAME=main ./music.sh "描述" [--lyrics "歌词"] [--instrumental]
27
+ ./music.sh --output-dir "$(pwd)" "描述" [--lyrics "歌词"] [--instrumental]
29
28
  ```
30
29
 
31
30
  ## 参数说明
@@ -34,6 +33,7 @@ WORKSPACE_NAME=main ./music.sh "描述" [--lyrics "歌词"] [--instrumental]
34
33
 
35
34
  | 参数 | 必填 | 说明 |
36
35
  |------|------|------|
36
+ | `--output-dir` | 是 | 输出目录的绝对路径,传入当前工作目录 |
37
37
  | `prompt` | 是 | 图片描述 |
38
38
  | `--aspect-ratio` | 否 | 比例,默认 1:1。可选:16:9, 9:16, 4:3 等 |
39
39
 
@@ -41,6 +41,7 @@ WORKSPACE_NAME=main ./music.sh "描述" [--lyrics "歌词"] [--instrumental]
41
41
 
42
42
  | 参数 | 必填 | 说明 |
43
43
  |------|------|------|
44
+ | `--output-dir` | 是 | 输出目录的绝对路径,传入当前工作目录 |
44
45
  | `prompt` | 是 | 音乐风格/情绪描述 |
45
46
  | `--lyrics` | 否 | 歌词文本 |
46
47
  | `--instrumental` | 否 | 纯音乐模式 |
@@ -2,16 +2,16 @@
2
2
  "output_dir": "media",
3
3
  "image": {
4
4
  "primary": {
5
- "provider": "minimax_image",
6
- "model": "image-01",
7
- "base_url": "https://api.minimaxi.com/v1",
8
- "api_key": "sk-cp-DC5lWd2Stt9CBFzLIT2awP4K-ZEn5AkYwjl3Cdj-mIBmgjxod518F2LaVF2L9c35Wv5-Eox0F1ctJD5vXtB9p3OmxoWLd9ge9zIUIMrCVuqBYdL_s6kb8Qs"
9
- },
10
- "fallback": {
11
5
  "provider": "vapi_image",
12
6
  "model": "nano-banana-pro",
13
7
  "base_url": "https://api.v3.cm/v1",
14
8
  "api_key": "sk-PXPUzqllWKJy2oj011Df510242264219Ba21093e3d2b2335"
9
+ },
10
+ "fallback": {
11
+ "provider": "minimax_image",
12
+ "model": "image-01",
13
+ "base_url": "https://api.minimaxi.com/v1",
14
+ "api_key": "sk-cp-DC5lWd2Stt9CBFzLIT2awP4K-ZEn5AkYwjl3Cdj-mIBmgjxod518F2LaVF2L9c35Wv5-Eox0F1ctJD5vXtB9p3OmxoWLd9ge9zIUIMrCVuqBYdL_s6kb8Qs"
15
15
  }
16
16
  },
17
17
  "music": {
@@ -2,8 +2,8 @@
2
2
  """
3
3
  统一资源生成调度器
4
4
  用法:
5
- python3 generate.py image "prompt" [--aspect-ratio 16:9] [--output path]
6
- python3 generate.py music "prompt" [--lyrics "歌词"] [--instrumental] [--output path]
5
+ python3 generate.py image "prompt" --output-dir /abs/path [--aspect-ratio 16:9] [--output path]
6
+ python3 generate.py music "prompt" --output-dir /abs/path [--lyrics "歌词"] [--instrumental] [--output path]
7
7
  """
8
8
  import argparse
9
9
  import json
@@ -22,32 +22,69 @@ def load_config():
22
22
  return json.load(f)
23
23
 
24
24
 
25
- def get_output_dir():
26
- """Resolve output directory. Fallback to /tmp/media/ if primary is unwritable."""
27
- workspace = os.environ.get("WORKSPACE_NAME", "main")
28
- openclaw_home = os.environ.get("OPENCLAW_HOME", os.path.join(os.path.expanduser("~"), ".openclaw"))
29
-
30
- if workspace == "main":
31
- base = os.path.join(openclaw_home, "workspace")
32
- else:
33
- base = os.path.join(openclaw_home, f"workspace-{workspace}")
34
-
35
- out_dir = os.path.join(base, "media")
25
+ def ensure_output_dir(path):
26
+ """Ensure output directory exists and is writable. Fallback to /tmp/media/ if not."""
36
27
  try:
37
- os.makedirs(out_dir, exist_ok=True)
38
- return out_dir
28
+ os.makedirs(path, exist_ok=True)
29
+ return path
39
30
  except OSError:
40
- # Primary dir unwritable (e.g. /root on macOS) → fallback to tmp
41
31
  fallback = os.path.join(tempfile.gettempdir(), "media")
42
32
  os.makedirs(fallback, exist_ok=True)
43
- print(f"[warn] {out_dir} not writable, using {fallback}", file=sys.stderr)
33
+ print(f"[warn] {path} not writable, using {fallback}", file=sys.stderr)
44
34
  return fallback
45
35
 
46
36
 
47
- def make_output_path(out_dir, resource_type, ext):
48
- """Generate timestamped filename."""
37
+ def ratio_to_size(ratio_str, max_dim=2048):
38
+ """Parse '16:9' style ratio and compute (width, height) capped at max_dim."""
39
+ if not ratio_str or ratio_str == "1:1":
40
+ return 1024, 1024
41
+
42
+ parts = ratio_str.split(":")
43
+ if len(parts) != 2:
44
+ return 1024, 1024
45
+
46
+ try:
47
+ rw, rh = float(parts[0]), float(parts[1])
48
+ except ValueError:
49
+ return 1024, 1024
50
+
51
+ # Scale so the longer side = max_dim
52
+ scale = max_dim / max(rw, rh)
53
+ w = round(rw * scale)
54
+ h = round(rh * scale)
55
+ return w, h
56
+
57
+
58
+ def make_output_path(out_dir, resource_type, model, params, ext):
59
+ """Generate filename with model name and key parameters.
60
+
61
+ Format: {type}_{model}_{params}_{timestamp}.{ext}
62
+ Example: image_nano-banana-pro_2048x1152_20260414_123456.png
63
+ """
49
64
  ts = datetime.now().strftime("%Y%m%d_%H%M%S")
50
- return os.path.join(out_dir, f"{resource_type}_{ts}.{ext}")
65
+
66
+ # Extract key parameters for filename
67
+ param_parts = []
68
+ if resource_type == "image":
69
+ aspect_ratio = params.get("aspect_ratio", "1:1")
70
+ # Convert ratio to actual dimensions
71
+ w, h = ratio_to_size(aspect_ratio)
72
+ param_parts.append(f"{w}x{h}")
73
+ elif resource_type == "music":
74
+ if params.get("instrumental"):
75
+ param_parts.append("instrumental")
76
+
77
+ # Build filename parts
78
+ # Clean model name: replace special chars with dash
79
+ clean_model = model.replace("/", "-").replace("_", "-")
80
+
81
+ parts = [resource_type, clean_model]
82
+ if param_parts:
83
+ parts.extend(param_parts)
84
+ parts.append(ts)
85
+
86
+ filename = "_".join(parts) + f".{ext}"
87
+ return os.path.join(out_dir, filename)
51
88
 
52
89
 
53
90
  def append_log(log_path, entry):
@@ -110,31 +147,51 @@ def main():
110
147
  parser.add_argument("--lyrics", default=None, help="Lyrics text (music only)")
111
148
  parser.add_argument("--instrumental", action="store_true", help="Instrumental mode (music only)")
112
149
  parser.add_argument("--output", default=None, help="Output file path")
150
+ parser.add_argument("--output-dir", required=True, help="Absolute path to output directory (required)")
113
151
  args = parser.parse_args()
114
152
 
115
- out_dir = get_output_dir()
153
+ out_dir = ensure_output_dir(args.output_dir)
116
154
 
155
+ # Prepare kwargs
117
156
  if args.type == "image":
118
- output_path = args.output or make_output_path(out_dir, "image", "png")
119
157
  kwargs = {
120
158
  "out_dir": out_dir,
121
- "output_path": output_path,
122
159
  "aspect_ratio": args.aspect_ratio,
123
160
  }
161
+ ext = "png"
124
162
  else:
125
- output_path = args.output or make_output_path(out_dir, "music", "mp3")
126
163
  kwargs = {
127
164
  "out_dir": out_dir,
128
- "output_path": output_path,
129
165
  "lyrics": args.lyrics,
130
166
  "instrumental": args.instrumental,
131
167
  }
168
+ ext = "mp3"
132
169
 
133
170
  start_time = time.time()
134
171
  start_dt = datetime.now()
135
172
 
136
173
  try:
174
+ # Handle output path: user-specified or auto-named with model info
175
+ if args.output:
176
+ # User specified output path - use it directly
177
+ kwargs["output_path"] = args.output
178
+ else:
179
+ # Auto-naming: generate to temp path first, then rename with model info
180
+ import tempfile
181
+ with tempfile.NamedTemporaryFile(delete=False, suffix=f".{ext}", dir=out_dir) as tmp:
182
+ tmp_path = tmp.name
183
+ kwargs["output_path"] = tmp_path
184
+
137
185
  files, used_provider = dispatch(args.type, args.prompt, **kwargs)
186
+
187
+ # Rename with proper filename containing model and params (only for auto-naming)
188
+ if not args.output:
189
+ model = used_provider["model"]
190
+ proper_path = make_output_path(out_dir, args.type, model, kwargs, ext)
191
+ import shutil
192
+ shutil.move(files[0], proper_path)
193
+ files = [proper_path]
194
+
138
195
  duration = round(time.time() - start_time, 2)
139
196
  end_dt = datetime.now()
140
197
 
@@ -16,5 +16,41 @@
16
16
  "started_at": "2026-04-13 14:11:12",
17
17
  "finished_at": "2026-04-13 14:11:36",
18
18
  "duration_seconds": 23.5
19
+ },
20
+ {
21
+ "id": "20260414_005846",
22
+ "type": "image",
23
+ "name": "test_output.png",
24
+ "files": [
25
+ "/tmp/test_output.png"
26
+ ],
27
+ "prompt": "a cute cat",
28
+ "params": {
29
+ "aspect_ratio": "1:1",
30
+ "output_path": "/tmp/test_output.png"
31
+ },
32
+ "provider": "vapi_image",
33
+ "model": "nano-banana-pro",
34
+ "started_at": "2026-04-14 00:58:46",
35
+ "finished_at": "2026-04-14 00:59:08",
36
+ "duration_seconds": 21.9
37
+ },
38
+ {
39
+ "id": "20260414_005910",
40
+ "type": "image",
41
+ "name": "image_nano-banana-pro_2048x1152_20260414_005930.png",
42
+ "files": [
43
+ "/Users/yiran/.openclaw/workspace/media/image_nano-banana-pro_2048x1152_20260414_005930.png"
44
+ ],
45
+ "prompt": "a beautiful sunset",
46
+ "params": {
47
+ "aspect_ratio": "16:9",
48
+ "output_path": "/Users/yiran/.openclaw/workspace/media/tmpdy8csqoa.png"
49
+ },
50
+ "provider": "vapi_image",
51
+ "model": "nano-banana-pro",
52
+ "started_at": "2026-04-14 00:59:10",
53
+ "finished_at": "2026-04-14 00:59:30",
54
+ "duration_seconds": 20.44
19
55
  }
20
56
  ]
@@ -1,32 +1,39 @@
1
1
  #!/bin/bash
2
2
  # 图片生成入口
3
- # 用法: WORKSPACE_NAME=main ./image.sh "描述" [--aspect-ratio 16:9]
3
+ # 用法: ./image.sh --output-dir /abs/path "描述" [--aspect-ratio 16:9]
4
4
  set -euo pipefail
5
5
 
6
- PROMPT="${1:-}"
7
- shift 2>/dev/null || true
8
-
9
- if [ -z "$PROMPT" ]; then
10
- echo "用法: WORKSPACE_NAME=xxx ./image.sh \"描述\" [--aspect-ratio 16:9]"
11
- echo "示例: WORKSPACE_NAME=main ./image.sh \"a cute cat\" --aspect-ratio 16:9"
12
- exit 1
13
- fi
14
-
6
+ OUTPUT_DIR=""
7
+ PROMPT=""
15
8
  ASPECT_RATIO=""
16
9
 
17
10
  while [ $# -gt 0 ]; do
18
11
  case "$1" in
12
+ --output-dir) OUTPUT_DIR="$2"; shift 2 ;;
19
13
  --aspect-ratio) ASPECT_RATIO="$2"; shift 2 ;;
20
- *) shift ;;
14
+ *)
15
+ if [ -z "$PROMPT" ]; then
16
+ PROMPT="$1"
17
+ fi
18
+ shift
19
+ ;;
21
20
  esac
22
21
  done
23
22
 
23
+ if [ -z "$OUTPUT_DIR" ] || [ -z "$PROMPT" ]; then
24
+ echo "用法: ./image.sh --output-dir <绝对路径> \"描述\" [--aspect-ratio 16:9]"
25
+ echo "示例: ./image.sh --output-dir /root/.openclaw/workspace/media \"a cute cat\" --aspect-ratio 16:9"
26
+ echo "注意: --output-dir 必须是绝对路径,传入当前工作目录即可"
27
+ exit 1
28
+ fi
29
+
24
30
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
25
31
 
26
32
  # Build args for generate.py
27
33
  ARGS=()
28
34
  ARGS+=("image")
29
35
  ARGS+=("$PROMPT")
36
+ ARGS+=(--output-dir "$OUTPUT_DIR")
30
37
  [ -n "$ASPECT_RATIO" ] && ARGS+=(--aspect-ratio "$ASPECT_RATIO")
31
38
 
32
39
  python3 "$SCRIPT_DIR/generate.py" "${ARGS[@]}"
@@ -1,34 +1,41 @@
1
1
  #!/bin/bash
2
2
  # 音乐生成入口
3
- # 用法: WORKSPACE_NAME=main ./music.sh "描述" [--lyrics "歌词"] [--instrumental]
3
+ # 用法: ./music.sh --output-dir /abs/path "描述" [--lyrics "歌词"] [--instrumental]
4
4
  set -euo pipefail
5
5
 
6
- PROMPT="${1:-}"
7
- shift 2>/dev/null || true
8
-
9
- if [ -z "$PROMPT" ]; then
10
- echo "用法: WORKSPACE_NAME=xxx ./music.sh \"描述\" [--lyrics \"歌词\"] [--instrumental]"
11
- echo "示例: WORKSPACE_NAME=main ./music.sh \"relaxing guitar\" --instrumental"
12
- exit 1
13
- fi
14
-
6
+ OUTPUT_DIR=""
7
+ PROMPT=""
15
8
  LYRICS=""
16
9
  INSTRUMENTAL=false
17
10
 
18
11
  while [ $# -gt 0 ]; do
19
12
  case "$1" in
13
+ --output-dir) OUTPUT_DIR="$2"; shift 2 ;;
20
14
  --lyrics) LYRICS="$2"; shift 2 ;;
21
15
  --instrumental) INSTRUMENTAL=true; shift ;;
22
- *) shift ;;
16
+ *)
17
+ if [ -z "$PROMPT" ]; then
18
+ PROMPT="$1"
19
+ fi
20
+ shift
21
+ ;;
23
22
  esac
24
23
  done
25
24
 
25
+ if [ -z "$OUTPUT_DIR" ] || [ -z "$PROMPT" ]; then
26
+ echo "用法: ./music.sh --output-dir <绝对路径> \"描述\" [--lyrics \"歌词\"] [--instrumental]"
27
+ echo "示例: ./music.sh --output-dir /root/.openclaw/workspace/media \"relaxing guitar\" --instrumental"
28
+ echo "注意: --output-dir 必须是绝对路径,传入当前工作目录即可"
29
+ exit 1
30
+ fi
31
+
26
32
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
27
33
 
28
34
  # Build args for generate.py
29
35
  ARGS=()
30
36
  ARGS+=("music")
31
37
  ARGS+=("$PROMPT")
38
+ ARGS+=(--output-dir "$OUTPUT_DIR")
32
39
  [ -n "$LYRICS" ] && ARGS+=(--lyrics "$LYRICS")
33
40
  [ "$INSTRUMENTAL" = true ] && ARGS+=(--instrumental)
34
41