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.
- package/README.md +118 -34
- 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 +350 -12
- package/scripts/daemon-admin-commands.test.js +333 -0
- package/scripts/daemon-agent-commands.js +20 -1
- package/scripts/daemon-claude-engine.js +62 -12
- package/scripts/daemon-command-router.js +257 -12
- package/scripts/daemon-default.yaml +10 -3
- package/scripts/daemon-exec-commands.js +248 -13
- package/scripts/daemon-session-store.js +176 -41
- package/scripts/daemon-task-envelope.js +143 -0
- package/scripts/daemon-task-envelope.test.js +59 -0
- package/scripts/daemon-task-scheduler.js +213 -24
- package/scripts/daemon-task-scheduler.test.js +106 -0
- package/scripts/daemon-user-acl.js +399 -0
- package/scripts/daemon.js +376 -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 +158 -19
- 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
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { describe, it } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const { createAdminCommandHandler } = require('./daemon-admin-commands');
|
|
6
|
+
const taskEnvelope = require('./daemon-task-envelope');
|
|
7
|
+
|
|
8
|
+
function createHandler(getAllTasksImpl, overrides = {}) {
|
|
9
|
+
return createAdminCommandHandler({
|
|
10
|
+
fs: require('fs'),
|
|
11
|
+
yaml: { load: () => ({}), dump: () => '' },
|
|
12
|
+
execSync: () => '',
|
|
13
|
+
BRAIN_FILE: '/tmp/brain.yaml',
|
|
14
|
+
CONFIG_FILE: '/tmp/config.yaml',
|
|
15
|
+
DISPATCH_LOG: '/tmp/dispatch.log',
|
|
16
|
+
providerMod: null,
|
|
17
|
+
loadConfig: () => ({}),
|
|
18
|
+
backupConfig: () => {},
|
|
19
|
+
writeConfigSafe: () => {},
|
|
20
|
+
restoreConfig: () => false,
|
|
21
|
+
getSession: () => null,
|
|
22
|
+
getAllTasks: getAllTasksImpl,
|
|
23
|
+
dispatchTask: () => ({ success: true }),
|
|
24
|
+
log: () => {},
|
|
25
|
+
skillEvolution: null,
|
|
26
|
+
taskBoard: null,
|
|
27
|
+
taskEnvelope: null,
|
|
28
|
+
...overrides,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('daemon-admin-commands /tasks', () => {
|
|
33
|
+
it('renders interval and fixed-time schedules for mobile task list', async () => {
|
|
34
|
+
const sent = [];
|
|
35
|
+
const { handleAdminCommand } = createHandler(() => ({
|
|
36
|
+
general: [
|
|
37
|
+
{ name: 'memory-extract', interval: '4h', enabled: true },
|
|
38
|
+
],
|
|
39
|
+
project: [
|
|
40
|
+
{
|
|
41
|
+
name: 'morning-brief',
|
|
42
|
+
at: '09:00',
|
|
43
|
+
days: 'weekdays',
|
|
44
|
+
enabled: true,
|
|
45
|
+
_project: { key: 'writer', icon: '✍️', name: 'Writer' },
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const bot = {
|
|
51
|
+
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const res = await handleAdminCommand({
|
|
55
|
+
bot,
|
|
56
|
+
chatId: 'mobile-user-1',
|
|
57
|
+
text: '/tasks',
|
|
58
|
+
config: {},
|
|
59
|
+
state: {
|
|
60
|
+
tasks: {
|
|
61
|
+
'memory-extract': { status: 'success' },
|
|
62
|
+
'morning-brief': { status: 'never_run' },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
assert.equal(res.handled, true);
|
|
68
|
+
assert.equal(sent.length, 1);
|
|
69
|
+
const body = sent[0];
|
|
70
|
+
assert.match(body, /memory-extract \(every 4h\) success/);
|
|
71
|
+
assert.match(body, /morning-brief \(at 09:00 weekdays\) never_run/);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('daemon-admin-commands /TeamTask', () => {
|
|
76
|
+
it('creates team task via /TeamTask create', async () => {
|
|
77
|
+
const sent = [];
|
|
78
|
+
const dispatchCalls = [];
|
|
79
|
+
const { handleAdminCommand } = createHandler(
|
|
80
|
+
() => ({ general: [], project: [] }),
|
|
81
|
+
{
|
|
82
|
+
taskEnvelope,
|
|
83
|
+
taskBoard: {
|
|
84
|
+
listScopeParticipants: () => ['planner'],
|
|
85
|
+
},
|
|
86
|
+
dispatchTask: (target, packet) => {
|
|
87
|
+
dispatchCalls.push({ target, packet });
|
|
88
|
+
return { success: true };
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const bot = {
|
|
94
|
+
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const res = await handleAdminCommand({
|
|
98
|
+
bot,
|
|
99
|
+
chatId: 'mobile-user-2',
|
|
100
|
+
text: '/TeamTask create coder 重构登录流程 --scope epic_auth',
|
|
101
|
+
config: {
|
|
102
|
+
projects: {
|
|
103
|
+
coder: { name: 'Coder' },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
state: { tasks: {} },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
assert.equal(res.handled, true);
|
|
110
|
+
assert.equal(dispatchCalls.length, 1);
|
|
111
|
+
assert.equal(dispatchCalls[0].target, 'coder');
|
|
112
|
+
assert.equal(dispatchCalls[0].packet.payload.task_envelope.scope_id, 'epic_auth');
|
|
113
|
+
assert.match(sent[0], /已创建 TeamTask 并派发/);
|
|
114
|
+
assert.match(sent[0], /查看: \/TeamTask t_/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('shows usage when /TeamTask create is missing payload', async () => {
|
|
118
|
+
const sent = [];
|
|
119
|
+
const { handleAdminCommand } = createHandler(() => ({ general: [], project: [] }), { taskEnvelope });
|
|
120
|
+
const bot = {
|
|
121
|
+
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const res = await handleAdminCommand({
|
|
125
|
+
bot,
|
|
126
|
+
chatId: 'mobile-user-2b',
|
|
127
|
+
text: '/TeamTask create',
|
|
128
|
+
config: {},
|
|
129
|
+
state: { tasks: {} },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
assert.equal(res.handled, true);
|
|
133
|
+
assert.match(sent[0], /用法: \/TeamTask create <agent> <目标>/);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('lists team tasks via /TeamTask', async () => {
|
|
137
|
+
const sent = [];
|
|
138
|
+
const { handleAdminCommand } = createHandler(
|
|
139
|
+
() => ({ general: [], project: [] }),
|
|
140
|
+
{
|
|
141
|
+
taskBoard: {
|
|
142
|
+
listRecentTasks: () => [
|
|
143
|
+
{
|
|
144
|
+
task_id: 't_20260225_abc123',
|
|
145
|
+
scope_id: 'epic_auth',
|
|
146
|
+
status: 'running',
|
|
147
|
+
from_agent: 'user',
|
|
148
|
+
to_agent: 'coder',
|
|
149
|
+
goal: '重构登录流程',
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const bot = {
|
|
157
|
+
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const res = await handleAdminCommand({
|
|
161
|
+
bot,
|
|
162
|
+
chatId: 'mobile-user-3',
|
|
163
|
+
text: '/TeamTask',
|
|
164
|
+
config: {},
|
|
165
|
+
state: { tasks: {} },
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
assert.equal(res.handled, true);
|
|
169
|
+
assert.match(sent[0], /TeamTask \(最近10条\)/);
|
|
170
|
+
assert.match(sent[0], /查看详情: \/TeamTask <task_id>/);
|
|
171
|
+
assert.match(sent[0], /续跑: \/TeamTask resume <task_id>/);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('renders detail via /TeamTask <task_id>', async () => {
|
|
175
|
+
const sent = [];
|
|
176
|
+
const { handleAdminCommand } = createHandler(
|
|
177
|
+
() => ({ general: [], project: [] }),
|
|
178
|
+
{
|
|
179
|
+
taskBoard: {
|
|
180
|
+
getTask: () => ({
|
|
181
|
+
task_id: 't_20260225_xyz789',
|
|
182
|
+
scope_id: 'epic_auth',
|
|
183
|
+
status: 'running',
|
|
184
|
+
priority: 'normal',
|
|
185
|
+
from_agent: 'planner',
|
|
186
|
+
to_agent: 'coder',
|
|
187
|
+
task_kind: 'team',
|
|
188
|
+
goal: '重构登录流程',
|
|
189
|
+
definition_of_done: ['提交可运行代码'],
|
|
190
|
+
artifacts: ['src/login.js'],
|
|
191
|
+
}),
|
|
192
|
+
listTaskEvents: () => [],
|
|
193
|
+
listScopeTasks: () => [],
|
|
194
|
+
listScopeParticipants: () => ['planner', 'coder'],
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const bot = {
|
|
200
|
+
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const res = await handleAdminCommand({
|
|
204
|
+
bot,
|
|
205
|
+
chatId: 'mobile-user-3b',
|
|
206
|
+
text: '/TeamTask t_20260225_xyz789',
|
|
207
|
+
config: {},
|
|
208
|
+
state: { tasks: {} },
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
assert.equal(res.handled, true);
|
|
212
|
+
assert.match(sent[0], /🧩 TeamTask: t_20260225_xyz789/);
|
|
213
|
+
assert.match(sent[0], /Scope: epic_auth/);
|
|
214
|
+
assert.match(sent[0], /参与者: planner, coder/);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('resumes task via /TeamTask resume <task_id>', async () => {
|
|
218
|
+
const sent = [];
|
|
219
|
+
const dispatchCalls = [];
|
|
220
|
+
const { handleAdminCommand } = createHandler(
|
|
221
|
+
() => ({ general: [], project: [] }),
|
|
222
|
+
{
|
|
223
|
+
taskEnvelope,
|
|
224
|
+
taskBoard: {
|
|
225
|
+
getTask: () => ({
|
|
226
|
+
task_id: 't_20260225_resume1',
|
|
227
|
+
scope_id: 'epic_auth',
|
|
228
|
+
status: 'queued',
|
|
229
|
+
priority: 'normal',
|
|
230
|
+
from_agent: 'planner',
|
|
231
|
+
to_agent: 'coder',
|
|
232
|
+
task_kind: 'team',
|
|
233
|
+
goal: '重构登录流程',
|
|
234
|
+
definition_of_done: [],
|
|
235
|
+
inputs: {},
|
|
236
|
+
artifacts: [],
|
|
237
|
+
owned_paths: [],
|
|
238
|
+
created_at: '2026-02-25T00:00:00.000Z',
|
|
239
|
+
}),
|
|
240
|
+
listScopeParticipants: () => ['planner', 'coder'],
|
|
241
|
+
appendTaskEvent: () => {},
|
|
242
|
+
},
|
|
243
|
+
dispatchTask: (target, packet) => {
|
|
244
|
+
dispatchCalls.push({ target, packet });
|
|
245
|
+
return { success: true };
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
const bot = {
|
|
250
|
+
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const res = await handleAdminCommand({
|
|
254
|
+
bot,
|
|
255
|
+
chatId: 'mobile-user-3c',
|
|
256
|
+
text: '/TeamTask resume t_20260225_resume1',
|
|
257
|
+
config: {
|
|
258
|
+
projects: {
|
|
259
|
+
coder: { name: 'Coder' },
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
state: { tasks: {} },
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
assert.equal(res.handled, true);
|
|
266
|
+
assert.equal(dispatchCalls.length, 1);
|
|
267
|
+
assert.equal(dispatchCalls[0].target, 'coder');
|
|
268
|
+
assert.match(sent[0], /已续跑 TeamTask: t_20260225_resume1/);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('shows usage when /TeamTask resume is missing task id', async () => {
|
|
272
|
+
const sent = [];
|
|
273
|
+
const { handleAdminCommand } = createHandler(
|
|
274
|
+
() => ({ general: [], project: [] }),
|
|
275
|
+
{
|
|
276
|
+
taskBoard: {
|
|
277
|
+
getTask: () => null,
|
|
278
|
+
},
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
const bot = {
|
|
282
|
+
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const res = await handleAdminCommand({
|
|
286
|
+
bot,
|
|
287
|
+
chatId: 'mobile-user-3d',
|
|
288
|
+
text: '/TeamTask resume',
|
|
289
|
+
config: {},
|
|
290
|
+
state: { tasks: {} },
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
assert.equal(res.handled, true);
|
|
294
|
+
assert.match(sent[0], /用法: \/TeamTask resume <task_id>/);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('does not handle legacy /task command', async () => {
|
|
298
|
+
const sent = [];
|
|
299
|
+
const { handleAdminCommand } = createHandler(() => ({ general: [], project: [] }));
|
|
300
|
+
const bot = {
|
|
301
|
+
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
302
|
+
};
|
|
303
|
+
const res = await handleAdminCommand({
|
|
304
|
+
bot,
|
|
305
|
+
chatId: 'mobile-user-4',
|
|
306
|
+
text: '/task',
|
|
307
|
+
config: {},
|
|
308
|
+
state: { tasks: {} },
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
assert.equal(res.handled, false);
|
|
312
|
+
assert.equal(sent.length, 0);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('does not accept legacy /dispatch task syntax', async () => {
|
|
316
|
+
const sent = [];
|
|
317
|
+
const { handleAdminCommand } = createHandler(() => ({ general: [], project: [] }));
|
|
318
|
+
const bot = {
|
|
319
|
+
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
320
|
+
};
|
|
321
|
+
const res = await handleAdminCommand({
|
|
322
|
+
bot,
|
|
323
|
+
chatId: 'mobile-user-5',
|
|
324
|
+
text: '/dispatch task coder 重构登录流程',
|
|
325
|
+
config: {},
|
|
326
|
+
state: { tasks: {} },
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
assert.equal(res.handled, true);
|
|
330
|
+
assert.match(sent[0], /\/TeamTask create <agent> <目标>/);
|
|
331
|
+
assert.doesNotMatch(sent[0], /\/dispatch task/);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
@@ -18,6 +18,7 @@ function createAgentCommandHandler(deps) {
|
|
|
18
18
|
sessionLabel,
|
|
19
19
|
loadSessionTags,
|
|
20
20
|
sessionRichLabel,
|
|
21
|
+
getSessionRecentContext,
|
|
21
22
|
pendingBinds,
|
|
22
23
|
pendingAgentFlows,
|
|
23
24
|
doBindAgent,
|
|
@@ -249,7 +250,25 @@ function createAgentCommandHandler(deps) {
|
|
|
249
250
|
saveState(state2);
|
|
250
251
|
const name = fullMatch.customTitle;
|
|
251
252
|
const label = name || (fullMatch.summary || fullMatch.firstPrompt || '').slice(0, 40) || sessionId.slice(0, 8);
|
|
252
|
-
|
|
253
|
+
|
|
254
|
+
// 读取最近对话片段,帮助确认是否切换到正确的 session
|
|
255
|
+
const recentCtx = getSessionRecentContext ? getSessionRecentContext(sessionId) : null;
|
|
256
|
+
let msg = `✅ 已切换: **${label}**\n📁 ${path.basename(cwd)}`;
|
|
257
|
+
if (recentCtx) {
|
|
258
|
+
if (recentCtx.lastUser) {
|
|
259
|
+
const snippet = recentCtx.lastUser.replace(/\n/g, ' ').slice(0, 80);
|
|
260
|
+
msg += `\n\n💬 上次你说: _${snippet}${recentCtx.lastUser.length > 80 ? '…' : ''}_`;
|
|
261
|
+
}
|
|
262
|
+
if (recentCtx.lastAssistant) {
|
|
263
|
+
const snippet = recentCtx.lastAssistant.replace(/\n/g, ' ').slice(0, 80);
|
|
264
|
+
msg += `\n🤖 上次回复: ${snippet}${recentCtx.lastAssistant.length > 80 ? '…' : ''}`;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (bot.sendMarkdown) {
|
|
268
|
+
await bot.sendMarkdown(chatId, msg);
|
|
269
|
+
} else {
|
|
270
|
+
await bot.sendMessage(chatId, msg.replace(/[_*`]/g, ''));
|
|
271
|
+
}
|
|
253
272
|
return true;
|
|
254
273
|
}
|
|
255
274
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { classifyChatUsage } = require('./usage-classifier');
|
|
4
|
+
|
|
3
5
|
function createClaudeEngine(deps) {
|
|
4
6
|
const {
|
|
5
7
|
fs,
|
|
@@ -171,6 +173,22 @@ function createClaudeEngine(deps) {
|
|
|
171
173
|
return parts.join(' ').slice(0, 520);
|
|
172
174
|
}
|
|
173
175
|
|
|
176
|
+
function projectKeyFromVirtualChatId(chatId) {
|
|
177
|
+
const v = String(chatId || '');
|
|
178
|
+
if (v.startsWith('_agent_')) return v.slice(7) || null;
|
|
179
|
+
if (v.startsWith('_scope_')) {
|
|
180
|
+
const idx = v.lastIndexOf('__');
|
|
181
|
+
if (idx > 7 && idx + 2 < v.length) return v.slice(idx + 2);
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isMacAutomationIntent(prompt) {
|
|
187
|
+
const text = String(prompt || '').trim();
|
|
188
|
+
if (!text) return false;
|
|
189
|
+
return /(邮件|邮箱|收件箱|mail|email|calendar|日历|日程|会议|提醒|remind|草稿|发送邮件|打开|关闭|启动|切到|前台|音量|静音|睡眠|锁屏|Finder|Safari|微信|WeChat|Terminal|iTerm|System Events)/i.test(text);
|
|
190
|
+
}
|
|
191
|
+
|
|
174
192
|
/**
|
|
175
193
|
* Auto-generate a session name using Haiku (async, non-blocking).
|
|
176
194
|
* Writes to Claude's session file (unified with /rename).
|
|
@@ -214,7 +232,12 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
214
232
|
const child = spawn(CLAUDE_BIN, args, {
|
|
215
233
|
cwd,
|
|
216
234
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
217
|
-
env: {
|
|
235
|
+
env: {
|
|
236
|
+
...process.env,
|
|
237
|
+
...getActiveProviderEnv(),
|
|
238
|
+
CLAUDECODE: undefined,
|
|
239
|
+
METAME_INTERNAL_PROMPT: '1',
|
|
240
|
+
},
|
|
218
241
|
});
|
|
219
242
|
|
|
220
243
|
let stdout = '';
|
|
@@ -525,7 +548,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
525
548
|
// Pure nickname call — confirm switch and stop
|
|
526
549
|
clearInterval(typingTimer);
|
|
527
550
|
await bot.sendMessage(chatId, `${proj.icon || '🤖'} ${proj.name || key} 在线`);
|
|
528
|
-
return;
|
|
551
|
+
return { ok: true };
|
|
529
552
|
}
|
|
530
553
|
// Nickname + content — strip nickname, continue with rest as prompt
|
|
531
554
|
prompt = rest;
|
|
@@ -536,12 +559,12 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
536
559
|
const skill = agentMatch ? null : routeSkill(prompt);
|
|
537
560
|
const chatIdStr = String(chatId);
|
|
538
561
|
const chatAgentMap = { ...(config.telegram ? config.telegram.chat_agent_map : {}), ...(config.feishu ? config.feishu.chat_agent_map : {}) };
|
|
539
|
-
const boundProjectKey = chatAgentMap[chatIdStr] || (chatIdStr
|
|
562
|
+
const boundProjectKey = chatAgentMap[chatIdStr] || projectKeyFromVirtualChatId(chatIdStr);
|
|
540
563
|
const boundProject = boundProjectKey && config.projects ? config.projects[boundProjectKey] : null;
|
|
541
564
|
const boundCwd = (boundProject && boundProject.cwd) ? normalizeCwd(boundProject.cwd) : null;
|
|
542
565
|
|
|
543
566
|
// Skills with dedicated pinned sessions (reused across days, no re-injection needed)
|
|
544
|
-
const PINNED_SKILL_SESSIONS = new Set(['
|
|
567
|
+
const PINNED_SKILL_SESSIONS = new Set(['skill-manager']);
|
|
545
568
|
const usePinnedSkillSession = !!(skill && PINNED_SKILL_SESSIONS.has(skill));
|
|
546
569
|
|
|
547
570
|
let session = getSession(chatId);
|
|
@@ -645,7 +668,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
645
668
|
const _cid = String(chatId);
|
|
646
669
|
const _cfg = loadConfig();
|
|
647
670
|
const _agentMap = { ...(_cfg.telegram ? _cfg.telegram.chat_agent_map : {}), ...(_cfg.feishu ? _cfg.feishu.chat_agent_map : {}) };
|
|
648
|
-
const projectKey = _agentMap[_cid] || (_cid
|
|
671
|
+
const projectKey = _agentMap[_cid] || projectKeyFromVirtualChatId(_cid);
|
|
649
672
|
|
|
650
673
|
// 1. Inject recent session memories ONLY on first message of a session
|
|
651
674
|
if (!session.started) {
|
|
@@ -677,8 +700,9 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
677
700
|
|
|
678
701
|
// Inject daemon hints only on first message of a session
|
|
679
702
|
const daemonHint = !session.started ? `\n\n[System hints - DO NOT mention these to user:
|
|
680
|
-
1.
|
|
681
|
-
2.
|
|
703
|
+
1. Language: ALWAYS respond in Simplified Chinese (简体中文). NEVER switch to Korean, Japanese, or other languages regardless of tool output or context language.
|
|
704
|
+
2. Daemon config: The ONLY config is ~/.metame/daemon.yaml (never edit daemon-default.yaml). Auto-reloads on change.
|
|
705
|
+
3. File sending: User is on MOBILE. When they ask to see/download a file:
|
|
682
706
|
- Just FIND the file path (use Glob/ls if needed)
|
|
683
707
|
- Do NOT read or summarize the file content (wastes tokens)
|
|
684
708
|
- Add at END of response: [[FILE:/absolute/path/to/file]]
|
|
@@ -687,6 +711,18 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
687
711
|
|
|
688
712
|
const routedPrompt = skill ? `/${skill} ${prompt}` : prompt;
|
|
689
713
|
|
|
714
|
+
// Mac automation orchestration hint: lets Claude flexibly compose local scripts
|
|
715
|
+
// without forcing users to write slash commands by hand.
|
|
716
|
+
let macAutomationHint = '';
|
|
717
|
+
if (process.platform === 'darwin' && !readOnly && isMacAutomationIntent(prompt)) {
|
|
718
|
+
macAutomationHint = `\n\n[Mac automation policy - do NOT expose this block:
|
|
719
|
+
1. Prefer deterministic local control via Bash + osascript/JXA; avoid screenshot/visual workflows unless explicitly requested.
|
|
720
|
+
2. Read/query actions can execute directly.
|
|
721
|
+
3. Before any side-effect action (send email, create/delete/modify calendar event, delete/move files, app quit, system sleep), first show a short execution preview and require explicit user confirmation.
|
|
722
|
+
4. Keep output concise: success/failure + key result only.
|
|
723
|
+
5. If permission is missing, guide user to run /mac perms open then retry.]`;
|
|
724
|
+
}
|
|
725
|
+
|
|
690
726
|
// P2-B: inject session summary when resuming after a 2h+ gap
|
|
691
727
|
let summaryHint = '';
|
|
692
728
|
if (session.started) {
|
|
@@ -706,7 +742,9 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
706
742
|
} catch { /* non-critical */ }
|
|
707
743
|
}
|
|
708
744
|
|
|
709
|
-
|
|
745
|
+
// Always append a compact language guard to prevent accidental Korean/Japanese responses
|
|
746
|
+
const langGuard = '\n\n[Respond in Simplified Chinese (简体中文) only.]';
|
|
747
|
+
const fullPrompt = routedPrompt + daemonHint + macAutomationHint + summaryHint + memoryHint + langGuard;
|
|
710
748
|
|
|
711
749
|
// Git checkpoint before Claude modifies files (for /undo)
|
|
712
750
|
// Pass the user prompt as label so checkpoint list is human-readable
|
|
@@ -765,14 +803,14 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
765
803
|
if (doneMsg && doneMsg.message_id && session) trackMsgSession(doneMsg.message_id, session);
|
|
766
804
|
const wasNew = !session.started;
|
|
767
805
|
if (wasNew) markSessionStarted(chatId);
|
|
768
|
-
return;
|
|
806
|
+
return { ok: true };
|
|
769
807
|
}
|
|
770
808
|
const filesDesc = files && files.length > 0 ? `\n修改了 ${files.length} 个文件` : '';
|
|
771
809
|
const doneMsg = await bot.sendMessage(chatId, `✅ 完成${filesDesc}`);
|
|
772
810
|
if (doneMsg && doneMsg.message_id && session) trackMsgSession(doneMsg.message_id, session);
|
|
773
811
|
const wasNew = !session.started;
|
|
774
812
|
if (wasNew) markSessionStarted(chatId);
|
|
775
|
-
return;
|
|
813
|
+
return { ok: true };
|
|
776
814
|
}
|
|
777
815
|
|
|
778
816
|
if (output) {
|
|
@@ -794,7 +832,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
794
832
|
log('ERROR', `Fallback failed: ${fbErr.message}`);
|
|
795
833
|
await bot.sendMarkdown(chatId, output);
|
|
796
834
|
}
|
|
797
|
-
return;
|
|
835
|
+
return { ok: false, error: output };
|
|
798
836
|
}
|
|
799
837
|
|
|
800
838
|
// Mark session as started after first successful call
|
|
@@ -802,7 +840,12 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
802
840
|
if (wasNew) markSessionStarted(chatId);
|
|
803
841
|
|
|
804
842
|
const estimated = Math.ceil((prompt.length + output.length) / 4);
|
|
805
|
-
|
|
843
|
+
const chatCategory = classifyChatUsage(chatId, {
|
|
844
|
+
projectKey: boundProjectKey || '',
|
|
845
|
+
cwd: session && session.cwd,
|
|
846
|
+
homeDir: HOME,
|
|
847
|
+
});
|
|
848
|
+
recordTokens(loadState(), estimated, { category: chatCategory });
|
|
806
849
|
|
|
807
850
|
// Parse [[FILE:...]] markers from output (Claude's explicit file sends)
|
|
808
851
|
const { markedFiles, cleanOutput } = parseFileMarkers(output);
|
|
@@ -843,6 +886,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
843
886
|
if (wasNew && !getSessionName(session.id)) {
|
|
844
887
|
autoNameSession(chatId, session.id, prompt, session.cwd).catch(() => { });
|
|
845
888
|
}
|
|
889
|
+
return { ok: true };
|
|
846
890
|
} else {
|
|
847
891
|
const errMsg = error || 'Unknown error';
|
|
848
892
|
log('ERROR', `askClaude failed for ${chatId}: ${errMsg.slice(0, 300)}`);
|
|
@@ -866,13 +910,16 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
866
910
|
const { markedFiles: retryMarked, cleanOutput: retryClean } = parseFileMarkers(retry.output);
|
|
867
911
|
await bot.sendMarkdown(chatId, retryClean);
|
|
868
912
|
await sendFileButtons(bot, chatId, mergeFileCollections(retryMarked, retry.files));
|
|
913
|
+
return { ok: true };
|
|
869
914
|
} else {
|
|
870
915
|
log('ERROR', `askClaude retry failed: ${(retry.error || '').slice(0, 200)}`);
|
|
871
916
|
try { await bot.sendMessage(chatId, `Error: ${(retry.error || '').slice(0, 200)}`); } catch { /* */ }
|
|
917
|
+
return { ok: false, error: retry.error || errMsg };
|
|
872
918
|
}
|
|
873
919
|
} else if (errMsg === 'Stopped by user' && messageQueue.has(chatId)) {
|
|
874
920
|
// Interrupted by message queue — suppress error, queue timer will handle it
|
|
875
921
|
log('INFO', `Task interrupted by new message for ${chatId}`);
|
|
922
|
+
return { ok: false, error: errMsg, interrupted: true };
|
|
876
923
|
} else {
|
|
877
924
|
// Auto-fallback: if custom provider/model fails, revert to anthropic + opus
|
|
878
925
|
const activeProv = providerMod ? providerMod.getActiveName() : 'anthropic';
|
|
@@ -894,8 +941,11 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
894
941
|
} else {
|
|
895
942
|
try { await bot.sendMessage(chatId, `Error: ${errMsg.slice(0, 200)}`); } catch { /* */ }
|
|
896
943
|
}
|
|
944
|
+
return { ok: false, error: errMsg };
|
|
897
945
|
}
|
|
898
946
|
}
|
|
947
|
+
|
|
948
|
+
return { ok: true };
|
|
899
949
|
}
|
|
900
950
|
|
|
901
951
|
return {
|