metame-cli 1.4.15 → 1.4.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/index.js +12 -5
- package/package.json +2 -2
- package/scripts/check-macos-control-capabilities.sh +77 -0
- package/scripts/daemon-admin-commands.js +441 -12
- package/scripts/daemon-admin-commands.test.js +333 -0
- package/scripts/daemon-claude-engine.js +71 -22
- package/scripts/daemon-command-router.js +242 -3
- package/scripts/daemon-default.yaml +10 -3
- package/scripts/daemon-exec-commands.js +248 -13
- package/scripts/daemon-task-envelope.js +143 -0
- package/scripts/daemon-task-envelope.test.js +59 -0
- package/scripts/daemon-task-scheduler.js +216 -24
- package/scripts/daemon-task-scheduler.test.js +106 -0
- package/scripts/daemon.js +374 -26
- package/scripts/distill.js +184 -34
- package/scripts/memory-extract.js +13 -5
- package/scripts/memory.js +239 -60
- package/scripts/providers.js +1 -1
- package/scripts/reliability-core.test.js +268 -0
- package/scripts/session-analytics.js +123 -35
- package/scripts/signal-capture.js +171 -11
- package/scripts/skill-evolution.js +288 -38
- package/scripts/skill-evolution.test.js +107 -0
- package/scripts/task-board.js +398 -0
- package/scripts/task-board.test.js +83 -0
- package/scripts/usage-classifier.js +139 -0
- package/scripts/utils.js +107 -0
- package/scripts/utils.test.js +61 -1
|
@@ -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,
|
|
@@ -17,8 +23,71 @@ function createAdminCommandHandler(deps) {
|
|
|
17
23
|
getAllTasks,
|
|
18
24
|
dispatchTask,
|
|
19
25
|
log,
|
|
26
|
+
skillEvolution,
|
|
27
|
+
taskBoard,
|
|
28
|
+
taskEnvelope,
|
|
20
29
|
} = deps;
|
|
21
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
|
+
|
|
22
91
|
async function handleAdminCommand(ctx) {
|
|
23
92
|
const { bot, chatId, text } = ctx;
|
|
24
93
|
const state = ctx.state || {};
|
|
@@ -40,6 +109,96 @@ function createAdminCommandHandler(deps) {
|
|
|
40
109
|
return { handled: true, config };
|
|
41
110
|
}
|
|
42
111
|
|
|
112
|
+
// /skill-evo — inspect and resolve skill evolution queue
|
|
113
|
+
if (text === '/skill-evo' || text.startsWith('/skill-evo ')) {
|
|
114
|
+
if (!skillEvolution) {
|
|
115
|
+
await bot.sendMessage(chatId, '❌ skill-evolution 模块不可用');
|
|
116
|
+
return { handled: true, config };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const arg = text.slice('/skill-evo'.length).trim();
|
|
120
|
+
const renderItem = (i) => {
|
|
121
|
+
const id = i.id || '-';
|
|
122
|
+
const target = i.skill_name ? `skill=${i.skill_name}` : (i.search_hint ? `hint=${i.search_hint}` : 'global');
|
|
123
|
+
const seen = i.last_seen || i.detected || '-';
|
|
124
|
+
const ev = i.evidence_count || 1;
|
|
125
|
+
return `- [${id}] ${i.type}/${i.status} (${target}, ev=${ev})\n ${i.reason || '(no reason)'}\n last: ${seen}`;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if (!arg || arg === 'list') {
|
|
129
|
+
const pendingAll = skillEvolution.listQueueItems({ status: 'pending', limit: 200 });
|
|
130
|
+
const notifiedAll = skillEvolution.listQueueItems({ status: 'notified', limit: 200 });
|
|
131
|
+
const installedAll = skillEvolution.listQueueItems({ status: 'installed', limit: 200 });
|
|
132
|
+
const dismissedAll = skillEvolution.listQueueItems({ status: 'dismissed', limit: 200 });
|
|
133
|
+
|
|
134
|
+
const pending = pendingAll.slice(0, 10);
|
|
135
|
+
const notified = notifiedAll.slice(0, 10);
|
|
136
|
+
const resolved = [...installedAll, ...dismissedAll]
|
|
137
|
+
.sort((a, b) => new Date(b.last_seen || b.detected || 0).getTime() - new Date(a.last_seen || a.detected || 0).getTime())
|
|
138
|
+
.slice(0, 5);
|
|
139
|
+
|
|
140
|
+
const lines = ['🧬 Skill Evolution Queue'];
|
|
141
|
+
lines.push(`pending: ${pendingAll.length} | notified: ${notifiedAll.length} | resolved(total): ${installedAll.length + dismissedAll.length}`);
|
|
142
|
+
if (pending.length > 0) {
|
|
143
|
+
lines.push('\nPending:');
|
|
144
|
+
for (const item of pending.slice(0, 5)) lines.push(renderItem(item));
|
|
145
|
+
}
|
|
146
|
+
if (notified.length > 0) {
|
|
147
|
+
lines.push('\nNotified:');
|
|
148
|
+
for (const item of notified.slice(0, 5)) lines.push(renderItem(item));
|
|
149
|
+
}
|
|
150
|
+
if (resolved.length > 0) {
|
|
151
|
+
lines.push('\nResolved (latest):');
|
|
152
|
+
for (const item of resolved) lines.push(renderItem(item));
|
|
153
|
+
}
|
|
154
|
+
if (pending.length === 0 && notified.length === 0 && resolved.length === 0) {
|
|
155
|
+
lines.push('\n(queue empty)');
|
|
156
|
+
}
|
|
157
|
+
lines.push('\n用法: /skill-evo done <id> | /skill-evo dismiss <id>');
|
|
158
|
+
|
|
159
|
+
await bot.sendMessage(chatId, lines.join('\n'));
|
|
160
|
+
|
|
161
|
+
if (bot.sendButtons) {
|
|
162
|
+
const actionable = [...notified, ...pending].slice(0, 3);
|
|
163
|
+
if (actionable.length > 0) {
|
|
164
|
+
const buttons = [];
|
|
165
|
+
for (const item of actionable) {
|
|
166
|
+
const label = `${item.type}:${(item.skill_name || item.search_hint || 'item').slice(0, 10)}`;
|
|
167
|
+
buttons.push([
|
|
168
|
+
{ text: `✅ ${label}`, callback_data: `/skill-evo done ${item.id}` },
|
|
169
|
+
{ text: `🙈 ${label}`, callback_data: `/skill-evo dismiss ${item.id}` },
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
await bot.sendButtons(chatId, '处理建议项:', buttons);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { handled: true, config };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const doneMatch = arg.match(/^(?:done|install|installed)\s+(\S+)$/i);
|
|
179
|
+
if (doneMatch) {
|
|
180
|
+
const id = doneMatch[1];
|
|
181
|
+
const ok = skillEvolution.resolveQueueItemById
|
|
182
|
+
? skillEvolution.resolveQueueItemById(id, 'installed')
|
|
183
|
+
: false;
|
|
184
|
+
await bot.sendMessage(chatId, ok ? `✅ 已标记 installed: ${id}` : `❌ 未找到可处理项: ${id}`);
|
|
185
|
+
return { handled: true, config };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const dismissMatch = arg.match(/^(?:dismiss|skip|ignored?)\s+(\S+)$/i);
|
|
189
|
+
if (dismissMatch) {
|
|
190
|
+
const id = dismissMatch[1];
|
|
191
|
+
const ok = skillEvolution.resolveQueueItemById
|
|
192
|
+
? skillEvolution.resolveQueueItemById(id, 'dismissed')
|
|
193
|
+
: false;
|
|
194
|
+
await bot.sendMessage(chatId, ok ? `✅ 已标记 dismissed: ${id}` : `❌ 未找到可处理项: ${id}`);
|
|
195
|
+
return { handled: true, config };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
await bot.sendMessage(chatId, '用法: /skill-evo list | /skill-evo done <id> | /skill-evo dismiss <id>');
|
|
199
|
+
return { handled: true, config };
|
|
200
|
+
}
|
|
201
|
+
|
|
43
202
|
if (text === '/tasks') {
|
|
44
203
|
const { general, project } = getAllTasks(config);
|
|
45
204
|
let msg = '';
|
|
@@ -47,7 +206,7 @@ function createAdminCommandHandler(deps) {
|
|
|
47
206
|
msg += '📋 General:\n';
|
|
48
207
|
for (const t of general) {
|
|
49
208
|
const ts = state.tasks[t.name] || {};
|
|
50
|
-
msg += `${t.enabled !== false ? '✅' : '⏸'} ${t.name} (${t
|
|
209
|
+
msg += `${t.enabled !== false ? '✅' : '⏸'} ${t.name} (${formatTaskSchedule(t)}) ${ts.status || 'never_run'}\n`;
|
|
51
210
|
}
|
|
52
211
|
}
|
|
53
212
|
// Project tasks grouped by _project
|
|
@@ -61,7 +220,7 @@ function createAdminCommandHandler(deps) {
|
|
|
61
220
|
msg += `\n${proj.icon} ${proj.name}:\n`;
|
|
62
221
|
for (const t of tasks) {
|
|
63
222
|
const ts = state.tasks[t.name] || {};
|
|
64
|
-
msg += `${t.enabled !== false ? '✅' : '⏸'} ${t.name} (${t
|
|
223
|
+
msg += `${t.enabled !== false ? '✅' : '⏸'} ${t.name} (${formatTaskSchedule(t)}) ${ts.status || 'never_run'}\n`;
|
|
65
224
|
}
|
|
66
225
|
}
|
|
67
226
|
if (!msg) {
|
|
@@ -72,6 +231,216 @@ function createAdminCommandHandler(deps) {
|
|
|
72
231
|
return { handled: true, config };
|
|
73
232
|
}
|
|
74
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
|
+
|
|
75
444
|
// /dispatch — inter-agent task dispatch
|
|
76
445
|
if (text.startsWith('/dispatch')) {
|
|
77
446
|
const args = text.slice('/dispatch'.length).trim();
|
|
@@ -125,21 +494,14 @@ function createAdminCommandHandler(deps) {
|
|
|
125
494
|
const prompt = toMatch[2].trim();
|
|
126
495
|
|
|
127
496
|
// Resolve target by project key or nickname
|
|
128
|
-
|
|
129
|
-
for (const [key, proj] of Object.entries(config.projects || {})) {
|
|
130
|
-
if (key === targetName || (proj.nicknames || []).some(n => n === targetName)) {
|
|
131
|
-
targetKey = key;
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
497
|
+
const targetKey = resolveProjectKey(targetName, config.projects || {});
|
|
135
498
|
if (!targetKey) {
|
|
136
499
|
await bot.sendMessage(chatId, `未找到 agent: ${targetName}\n可用: ${Object.keys(config.projects || {}).join(', ')}`);
|
|
137
500
|
return { handled: true, config };
|
|
138
501
|
}
|
|
139
502
|
|
|
140
503
|
// Determine sender from current chat's project mapping
|
|
141
|
-
const
|
|
142
|
-
const senderKey = chatAgentMap[chatId] || 'user';
|
|
504
|
+
const senderKey = resolveSenderKey(chatId, config);
|
|
143
505
|
|
|
144
506
|
const projInfo = config.projects[targetKey] || {};
|
|
145
507
|
// Find the target project's own Feishu chat (reverse lookup of chat_agent_map)
|
|
@@ -172,7 +534,14 @@ function createAdminCommandHandler(deps) {
|
|
|
172
534
|
return { handled: true, config };
|
|
173
535
|
}
|
|
174
536
|
|
|
175
|
-
await bot.sendMessage(chatId,
|
|
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'));
|
|
176
545
|
return { handled: true, config };
|
|
177
546
|
}
|
|
178
547
|
|
|
@@ -183,6 +552,66 @@ function createAdminCommandHandler(deps) {
|
|
|
183
552
|
return { handled: true, config };
|
|
184
553
|
}
|
|
185
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
|
+
|
|
186
615
|
if (text === '/quiet') {
|
|
187
616
|
try {
|
|
188
617
|
const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
|