metame-cli 1.4.17 → 1.4.19

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.
@@ -1,5 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const {
4
+ USAGE_CATEGORY_ORDER,
5
+ CORE_USAGE_CATEGORIES,
6
+ USAGE_CATEGORY_LABEL,
7
+ } = require('./usage-classifier');
8
+
3
9
  function createAdminCommandHandler(deps) {
4
10
  const {
5
11
  fs,
@@ -18,8 +24,70 @@ function createAdminCommandHandler(deps) {
18
24
  dispatchTask,
19
25
  log,
20
26
  skillEvolution,
27
+ taskBoard,
28
+ taskEnvelope,
21
29
  } = deps;
22
30
 
31
+ function resolveProjectKey(targetName, projects) {
32
+ if (!targetName || !projects) return null;
33
+ for (const [key, proj] of Object.entries(projects || {})) {
34
+ const nicknames = Array.isArray(proj.nicknames)
35
+ ? proj.nicknames
36
+ : (proj.nicknames ? [proj.nicknames] : []);
37
+ if (key === targetName || nicknames.some(n => n === targetName)) return key;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ function resolveSenderKey(chatId, config) {
43
+ const map = {
44
+ ...(config && config.feishu ? config.feishu.chat_agent_map : {}),
45
+ ...(config && config.telegram ? config.telegram.chat_agent_map : {}),
46
+ };
47
+ return map[String(chatId)] || 'user';
48
+ }
49
+
50
+ function popFlag(input, flagName) {
51
+ const src = String(input || '');
52
+ const re = new RegExp(`(?:^|\\s)--${flagName}\\s+(\\S+)`, 'i');
53
+ const m = src.match(re);
54
+ if (!m) return { text: src.trim(), value: '' };
55
+ const value = String(m[1] || '').trim();
56
+ const text = src.replace(m[0], ' ').replace(/\s+/g, ' ').trim();
57
+ return { text, value };
58
+ }
59
+
60
+ function parseTeamTaskArgs(raw) {
61
+ const src = String(raw || '').trim();
62
+ const first = src.match(/^(\S+)\s+([\s\S]+)$/);
63
+ if (!first) return null;
64
+ const targetName = first[1];
65
+ let rest = first[2].trim();
66
+ const scopePop = popFlag(rest, 'scope');
67
+ rest = scopePop.text;
68
+ const parentPop = popFlag(rest, 'parent');
69
+ rest = parentPop.text;
70
+ return {
71
+ targetName,
72
+ goal: rest,
73
+ scopeId: scopePop.value || '',
74
+ parentTaskId: parentPop.value || '',
75
+ };
76
+ }
77
+
78
+ function formatTaskSchedule(task) {
79
+ const at = typeof task.at === 'string' ? task.at.trim() : '';
80
+ if (at) {
81
+ const rawDays = task.days !== undefined ? task.days : task.weekdays;
82
+ let daysLabel = '';
83
+ if (Array.isArray(rawDays)) daysLabel = rawDays.join(',');
84
+ else if (typeof rawDays === 'string') daysLabel = rawDays.trim();
85
+ return daysLabel ? `at ${at} ${daysLabel}` : `at ${at}`;
86
+ }
87
+ if (task.interval) return `every ${task.interval}`;
88
+ return 'unspecified';
89
+ }
90
+
23
91
  async function handleAdminCommand(ctx) {
24
92
  const { bot, chatId, text } = ctx;
25
93
  const state = ctx.state || {};
@@ -138,7 +206,7 @@ function createAdminCommandHandler(deps) {
138
206
  msg += '📋 General:\n';
139
207
  for (const t of general) {
140
208
  const ts = state.tasks[t.name] || {};
141
- msg += `${t.enabled !== false ? '✅' : '⏾'} ${t.name} (${t.interval}) ${ts.status || 'never_run'}\n`;
209
+ msg += `${t.enabled !== false ? '✅' : '⏾'} ${t.name} (${formatTaskSchedule(t)}) ${ts.status || 'never_run'}\n`;
142
210
  }
143
211
  }
144
212
  // Project tasks grouped by _project
@@ -152,7 +220,7 @@ function createAdminCommandHandler(deps) {
152
220
  msg += `\n${proj.icon} ${proj.name}:\n`;
153
221
  for (const t of tasks) {
154
222
  const ts = state.tasks[t.name] || {};
155
- msg += `${t.enabled !== false ? '✅' : '⏾'} ${t.name} (${t.interval}) ${ts.status || 'never_run'}\n`;
223
+ msg += `${t.enabled !== false ? '✅' : '⏾'} ${t.name} (${formatTaskSchedule(t)}) ${ts.status || 'never_run'}\n`;
156
224
  }
157
225
  }
158
226
  if (!msg) {
@@ -163,6 +231,216 @@ function createAdminCommandHandler(deps) {
163
231
  return { handled: true, config };
164
232
  }
165
233
 
234
+ // /TeamTask — create/list/detail/resume team collaboration tasks
235
+ const teamTaskCmdMatch = text.match(/^\/teamtask(?:\s+([\s\S]+))?$/i);
236
+ if (teamTaskCmdMatch) {
237
+ const args = String(teamTaskCmdMatch[1] || '').trim();
238
+ if (/^create$/i.test(args)) {
239
+ await bot.sendMessage(chatId, '❌ 甚法: /TeamTask create <agent> <目标> [--scope <scopeId>] [--parent <taskId>]');
240
+ return { handled: true, config };
241
+ }
242
+ const createMatch = args.match(/^create\s+([\s\S]+)$/i);
243
+ if (createMatch) {
244
+ if (!taskEnvelope) {
245
+ await bot.sendMessage(chatId, '❌ task protocol 䞍可甚');
246
+ return { handled: true, config };
247
+ }
248
+ const parsed = parseTeamTaskArgs(createMatch[1]);
249
+ if (!parsed || !parsed.targetName || !parsed.goal) {
250
+ await bot.sendMessage(chatId, '❌ 甚法: /TeamTask create <agent> <目标> [--scope <scopeId>] [--parent <taskId>]');
251
+ return { handled: true, config };
252
+ }
253
+ const { targetName, goal, scopeId, parentTaskId } = parsed;
254
+ const targetKey = resolveProjectKey(targetName, config.projects || {});
255
+ if (!targetKey) {
256
+ await bot.sendMessage(chatId, `未扟到 agent: ${targetName}\n可甚: ${Object.keys(config.projects || {}).join(', ')}`);
257
+ return { handled: true, config };
258
+ }
259
+ const senderKey = resolveSenderKey(chatId, config);
260
+ const participants = (scopeId && taskBoard && taskBoard.listScopeParticipants)
261
+ ? taskBoard.listScopeParticipants(scopeId)
262
+ : [];
263
+ participants.push(senderKey, targetKey);
264
+ const envelope = taskEnvelope.normalizeTaskEnvelope({
265
+ from_agent: senderKey,
266
+ to_agent: targetKey,
267
+ scope_id: scopeId || '',
268
+ parent_task_id: parentTaskId || null,
269
+ participants,
270
+ goal,
271
+ task_kind: 'team',
272
+ definition_of_done: [
273
+ '蟓出可执行结果和关键结论',
274
+ '必芁时给出产物路埄䞎䞋䞀步建议',
275
+ ],
276
+ inputs: {
277
+ source_chat_id: String(chatId),
278
+ source: 'mobile_teamtask',
279
+ },
280
+ priority: 'normal',
281
+ status: 'queued',
282
+ });
283
+ const checked = taskEnvelope.validateTaskEnvelope(envelope);
284
+ if (!checked.ok) {
285
+ await bot.sendMessage(chatId, `❌ TeamTask 无效: ${checked.error}`);
286
+ return { handled: true, config };
287
+ }
288
+ const result = dispatchTask(targetKey, {
289
+ from: senderKey,
290
+ type: 'task',
291
+ priority: envelope.priority,
292
+ payload: {
293
+ title: goal.slice(0, 60),
294
+ prompt: goal,
295
+ task_envelope: envelope,
296
+ },
297
+ callback: false,
298
+ }, config);
299
+ if (result.success) {
300
+ await bot.sendMessage(chatId, [
301
+ `✅ 已创建 TeamTask 并掟发: ${envelope.task_id}`,
302
+ `Scope: ${envelope.scope_id || envelope.task_id}`,
303
+ `查看: /TeamTask ${envelope.task_id}`,
304
+ ].join('\n'));
305
+ } else {
306
+ await bot.sendMessage(chatId, `❌ 创建 TeamTask 倱莥: ${result.error}`);
307
+ }
308
+ return { handled: true, config };
309
+ }
310
+
311
+ if (!taskBoard) {
312
+ await bot.sendMessage(chatId, '❌ Task Board 䞍可甚');
313
+ return { handled: true, config };
314
+ }
315
+
316
+ if (!args || /^list$/i.test(args)) {
317
+ const recent = taskBoard.listRecentTasks(10, null, 'team');
318
+ if (recent.length === 0) {
319
+ await bot.sendMessage(chatId, '暂无 TeamTask。\n䜿甚 /TeamTask create <agent> <goal> 创建。');
320
+ return { handled: true, config };
321
+ }
322
+ let msg = '🧩 TeamTask (最近10条)\n';
323
+ for (const t of recent) {
324
+ msg += `\n- ${t.task_id} [${t.status}] scope=${t.scope_id || t.task_id}\n ${t.from_agent}→${t.to_agent} · ${t.goal.slice(0, 80)}`;
325
+ }
326
+ msg += '\n\n查看诊情: /TeamTask <task_id>\n续跑: /TeamTask resume <task_id>';
327
+ await bot.sendMessage(chatId, msg);
328
+ return { handled: true, config };
329
+ }
330
+
331
+ const resumeMatch = args.match(/^resume\s+(\S+)$/i);
332
+ if (resumeMatch) {
333
+ const taskId = resumeMatch[1];
334
+ const task = taskBoard.getTask(taskId);
335
+ if (!task || task.task_kind !== 'team') {
336
+ await bot.sendMessage(chatId, `❌ 未扟到 TeamTask: ${taskId}`);
337
+ return { handled: true, config };
338
+ }
339
+ const targetKey = task.to_agent;
340
+ if (!config.projects || !config.projects[targetKey]) {
341
+ await bot.sendMessage(chatId, `❌ 目标 agent 䞍存圚: ${targetKey}`);
342
+ return { handled: true, config };
343
+ }
344
+ const envelope = taskEnvelope && taskEnvelope.normalizeTaskEnvelope
345
+ ? taskEnvelope.normalizeTaskEnvelope({
346
+ ...task,
347
+ status: 'queued',
348
+ updated_at: new Date().toISOString(),
349
+ task_kind: 'team',
350
+ participants: taskBoard.listScopeParticipants(task.scope_id || task.task_id),
351
+ }, {
352
+ from_agent: task.from_agent || resolveSenderKey(chatId, config),
353
+ to_agent: targetKey,
354
+ scope_id: task.scope_id || task.task_id,
355
+ })
356
+ : {
357
+ task_id: task.task_id,
358
+ scope_id: task.scope_id || task.task_id,
359
+ from_agent: task.from_agent || resolveSenderKey(chatId, config),
360
+ to_agent: targetKey,
361
+ participants: taskBoard.listScopeParticipants(task.scope_id || task.task_id),
362
+ goal: task.goal,
363
+ definition_of_done: task.definition_of_done || [],
364
+ inputs: task.inputs || {},
365
+ artifacts: task.artifacts || [],
366
+ owned_paths: task.owned_paths || [],
367
+ priority: task.priority || 'normal',
368
+ status: 'queued',
369
+ task_kind: 'team',
370
+ created_at: task.created_at,
371
+ updated_at: new Date().toISOString(),
372
+ };
373
+
374
+ const result = dispatchTask(targetKey, {
375
+ from: envelope.from_agent || 'user',
376
+ type: 'task',
377
+ priority: envelope.priority || 'normal',
378
+ payload: {
379
+ title: envelope.goal.slice(0, 60),
380
+ prompt: envelope.goal,
381
+ task_envelope: envelope,
382
+ },
383
+ callback: false,
384
+ new_session: false,
385
+ }, config);
386
+
387
+ if (result.success) {
388
+ taskBoard.appendTaskEvent(task.task_id, 'task_resume_requested', String(chatId), { by: String(chatId) });
389
+ await bot.sendMessage(chatId, `✅ 已续跑 TeamTask: ${task.task_id}`);
390
+ } else {
391
+ await bot.sendMessage(chatId, `❌ 续跑倱莥: ${result.error}`);
392
+ }
393
+ return { handled: true, config };
394
+ }
395
+
396
+ if (/^resume$/i.test(args)) {
397
+ await bot.sendMessage(chatId, '❌ 甚法: /TeamTask resume <task_id>');
398
+ return { handled: true, config };
399
+ }
400
+
401
+ const task = taskBoard.getTask(args);
402
+ if (!task || task.task_kind !== 'team') {
403
+ await bot.sendMessage(chatId, `❌ 未扟到 TeamTask: ${args}`);
404
+ return { handled: true, config };
405
+ }
406
+ const events = taskBoard.listTaskEvents(task.task_id, 8);
407
+ const scopeId = task.scope_id || task.task_id;
408
+ const scopeTasks = taskBoard.listScopeTasks(scopeId, 12);
409
+ const scopeParticipants = taskBoard.listScopeParticipants(scopeId);
410
+ let detail = [
411
+ `🧩 TeamTask: ${task.task_id}`,
412
+ `Scope: ${scopeId}`,
413
+ `状态: ${task.status}`,
414
+ `䌘先级: ${task.priority}`,
415
+ `流向: ${task.from_agent} → ${task.to_agent}`,
416
+ `目标: ${task.goal}`,
417
+ ];
418
+ if (scopeParticipants.length > 0) {
419
+ detail.push(`参䞎者: ${scopeParticipants.join(', ')}`);
420
+ }
421
+ if (Array.isArray(task.definition_of_done) && task.definition_of_done.length > 0) {
422
+ detail.push('DoD:');
423
+ for (const d of task.definition_of_done.slice(0, 6)) detail.push(`- ${d}`);
424
+ }
425
+ if (Array.isArray(task.artifacts) && task.artifacts.length > 0) {
426
+ detail.push('产物:');
427
+ for (const a of task.artifacts.slice(0, 6)) detail.push(`- ${a}`);
428
+ }
429
+ if (task.last_error) detail.push(`错误: ${task.last_error.slice(0, 180)}`);
430
+ if (events.length > 0) {
431
+ detail.push('最近事件:');
432
+ for (const ev of events.slice(0, 5)) detail.push(`- [${ev.event_type}] ${ev.actor} @ ${ev.created_at}`);
433
+ }
434
+ if (scopeTasks.length > 1) {
435
+ detail.push('同 Scope 盞关任务:');
436
+ for (const st of scopeTasks.filter(x => x.task_id !== task.task_id).slice(0, 5)) {
437
+ detail.push(`- ${st.task_id} [${st.status}] ${st.from_agent}→${st.to_agent}`);
438
+ }
439
+ }
440
+ await bot.sendMessage(chatId, detail.join('\n'));
441
+ return { handled: true, config };
442
+ }
443
+
166
444
  // /dispatch — inter-agent task dispatch
167
445
  if (text.startsWith('/dispatch')) {
168
446
  const args = text.slice('/dispatch'.length).trim();
@@ -216,21 +494,14 @@ function createAdminCommandHandler(deps) {
216
494
  const prompt = toMatch[2].trim();
217
495
 
218
496
  // Resolve target by project key or nickname
219
- let targetKey = null;
220
- for (const [key, proj] of Object.entries(config.projects || {})) {
221
- if (key === targetName || (proj.nicknames || []).some(n => n === targetName)) {
222
- targetKey = key;
223
- break;
224
- }
225
- }
497
+ const targetKey = resolveProjectKey(targetName, config.projects || {});
226
498
  if (!targetKey) {
227
499
  await bot.sendMessage(chatId, `未扟到 agent: ${targetName}\n可甚: ${Object.keys(config.projects || {}).join(', ')}`);
228
500
  return { handled: true, config };
229
501
  }
230
502
 
231
503
  // Determine sender from current chat's project mapping
232
- const chatAgentMap = (config.feishu && config.feishu.chat_agent_map) || {};
233
- const senderKey = chatAgentMap[chatId] || 'user';
504
+ const senderKey = resolveSenderKey(chatId, config);
234
505
 
235
506
  const projInfo = config.projects[targetKey] || {};
236
507
  // Find the target project's own Feishu chat (reverse lookup of chat_agent_map)
@@ -263,7 +534,14 @@ function createAdminCommandHandler(deps) {
263
534
  return { handled: true, config };
264
535
  }
265
536
 
266
- await bot.sendMessage(chatId, '甚法:\n/dispatch status — 查看状态\n/dispatch log — 查看记圕\n/dispatch to <agent> <任务内容>');
537
+ await bot.sendMessage(chatId, [
538
+ '甚法:',
539
+ '/dispatch status — 查看状态',
540
+ '/dispatch log — 查看记圕',
541
+ '/dispatch to <agent> <任务内容> — 盎接跚 agent 掟发',
542
+ '/TeamTask create <agent> <目标> [--scope <id>] [--parent <id>] — 创建/续接 TeamTask',
543
+ '/TeamTask — 查看 TeamTask 列衚',
544
+ ].join('\n'));
267
545
  return { handled: true, config };
268
546
  }
269
547
 
@@ -274,6 +552,66 @@ function createAdminCommandHandler(deps) {
274
552
  return { handled: true, config };
275
553
  }
276
554
 
555
+ if (text === '/usage' || text.startsWith('/usage ')) {
556
+ const arg = text.slice('/usage'.length).trim() || 'today';
557
+ const usage = state.usage || {};
558
+ const daily = usage.daily || {};
559
+ const categories = usage.categories || {};
560
+ const limit = (config.budget && config.budget.daily_limit) || 50000;
561
+ const todayIso = new Date().toISOString().slice(0, 10);
562
+
563
+ // Resolve date range
564
+ let days = 1;
565
+ if (arg === 'week') days = 7;
566
+ else if (arg === 'month') days = 30;
567
+ else if (/^\d+d$/.test(arg)) days = Math.min(90, parseInt(arg, 10));
568
+
569
+ const dates = [];
570
+ for (let i = days - 1; i >= 0; i--) {
571
+ const d = new Date(`${todayIso}T00:00:00.000Z`);
572
+ d.setUTCDate(d.getUTCDate() - i);
573
+ dates.push(d.toISOString().slice(0, 10));
574
+ }
575
+
576
+ // Aggregate tokens by category across the date window
577
+ const totals = {};
578
+ let grandTotal = 0;
579
+ for (const date of dates) {
580
+ const bucket = daily[date] || {};
581
+ for (const [key, val] of Object.entries(bucket)) {
582
+ if (key === 'total') continue;
583
+ const n = Math.max(0, Math.floor(Number(val) || 0));
584
+ totals[key] = (totals[key] || 0) + n;
585
+ grandTotal += n;
586
+ }
587
+ }
588
+ // Fallback: if no daily breakdown yet, use categories totals for today
589
+ if (grandTotal === 0 && days === 1) {
590
+ for (const [key, meta] of Object.entries(categories)) {
591
+ const n = Math.max(0, Math.floor(Number(meta && meta.total) || 0));
592
+ if (n > 0) { totals[key] = n; grandTotal += n; }
593
+ }
594
+ }
595
+
596
+ const label = days === 1 ? `今日 (${todayIso})` : `近 ${days} 倩`;
597
+ const budgetPct = limit > 0 ? ((grandTotal / limit) * 100).toFixed(1) : '—';
598
+ let lines = [`📊 Token 甹量 — ${label}`, `合计: ${grandTotal.toLocaleString()} / ${limit.toLocaleString()} tokens (${budgetPct}%)`];
599
+
600
+ // Render by canonical order, then extras
601
+ const orderedKeys = [...USAGE_CATEGORY_ORDER, ...Object.keys(totals).filter(k => !USAGE_CATEGORY_ORDER.includes(k))];
602
+ for (const key of orderedKeys) {
603
+ const n = totals[key] || 0;
604
+ if (n === 0 && !CORE_USAGE_CATEGORIES.includes(key)) continue;
605
+ const pct = grandTotal > 0 ? ((n / grandTotal) * 100).toFixed(1) : '0.0';
606
+ const lbl = USAGE_CATEGORY_LABEL[key] || key;
607
+ const bar = '█'.repeat(Math.round(Number(pct) / 10)).padEnd(10, '░');
608
+ lines.push(`${lbl}: ${n.toLocaleString()} tokens (${pct}%) ${bar}`);
609
+ }
610
+
611
+ await bot.sendMessage(chatId, lines.join('\n'));
612
+ return { handled: true, config };
613
+ }
614
+
277
615
  if (text === '/quiet') {
278
616
  try {
279
617
  const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};