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
|
@@ -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
|
+
});
|
|
@@ -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,
|
|
@@ -42,13 +44,6 @@ function createClaudeEngine(deps) {
|
|
|
42
44
|
const SESSION_CWD_VALIDATION_TTL_MS = 30 * 1000;
|
|
43
45
|
const _sessionCwdValidationCache = new Map(); // key: `${sessionId}@@${cwd}` -> { inCwd, ts }
|
|
44
46
|
|
|
45
|
-
function decodeProjectDirName(dirName) {
|
|
46
|
-
const raw = String(dirName || '');
|
|
47
|
-
if (!raw) return '';
|
|
48
|
-
if (raw.startsWith('-')) return '/' + raw.slice(1).replace(/-/g, '/');
|
|
49
|
-
return raw.replace(/-/g, '/');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
47
|
function cacheSessionCwdValidation(cacheKey, inCwd) {
|
|
53
48
|
_sessionCwdValidationCache.set(cacheKey, { inCwd: !!inCwd, ts: Date.now() });
|
|
54
49
|
if (_sessionCwdValidationCache.size > 512) {
|
|
@@ -84,14 +79,23 @@ function createClaudeEngine(deps) {
|
|
|
84
79
|
if (entry && entry.projectPath) {
|
|
85
80
|
return cacheSessionCwdValidation(cacheKey, normalizeCwd(entry.projectPath) === normCwd);
|
|
86
81
|
}
|
|
82
|
+
// sessions-index may lag behind new sessions; use project-level path from any entry.
|
|
83
|
+
const anyProjectPath = (entries.find(e => e && e.projectPath) || {}).projectPath;
|
|
84
|
+
if (anyProjectPath) {
|
|
85
|
+
return cacheSessionCwdValidation(cacheKey, normalizeCwd(anyProjectPath) === normCwd);
|
|
86
|
+
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
// Weak fallback: encode normCwd using Claude's folder convention and accept
|
|
90
|
+
// only positive match. If it doesn't match, keep current session to avoid
|
|
91
|
+
// false mismatches for paths with non-ASCII/special characters.
|
|
92
|
+
const expectedDirName = '-' + normCwd.replace(/^\//, '').replace(/[\/_ ]/g, '-');
|
|
93
|
+
const actualDirName = path.basename(projectDir);
|
|
94
|
+
if (actualDirName === expectedDirName) {
|
|
95
|
+
return cacheSessionCwdValidation(cacheKey, true);
|
|
93
96
|
}
|
|
94
|
-
|
|
97
|
+
// Unable to prove mismatch safely.
|
|
98
|
+
return cacheSessionCwdValidation(cacheKey, true);
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
// Ultimate fallback (legacy path): scoped scan in target cwd.
|
|
@@ -169,6 +173,22 @@ function createClaudeEngine(deps) {
|
|
|
169
173
|
return parts.join(' ').slice(0, 520);
|
|
170
174
|
}
|
|
171
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
|
+
|
|
172
192
|
/**
|
|
173
193
|
* Auto-generate a session name using Haiku (async, non-blocking).
|
|
174
194
|
* Writes to Claude's session file (unified with /rename).
|
|
@@ -212,7 +232,12 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
212
232
|
const child = spawn(CLAUDE_BIN, args, {
|
|
213
233
|
cwd,
|
|
214
234
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
215
|
-
env: {
|
|
235
|
+
env: {
|
|
236
|
+
...process.env,
|
|
237
|
+
...getActiveProviderEnv(),
|
|
238
|
+
CLAUDECODE: undefined,
|
|
239
|
+
METAME_INTERNAL_PROMPT: '1',
|
|
240
|
+
},
|
|
216
241
|
});
|
|
217
242
|
|
|
218
243
|
let stdout = '';
|
|
@@ -523,7 +548,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
523
548
|
// Pure nickname call — confirm switch and stop
|
|
524
549
|
clearInterval(typingTimer);
|
|
525
550
|
await bot.sendMessage(chatId, `${proj.icon || '🤖'} ${proj.name || key} 在线`);
|
|
526
|
-
return;
|
|
551
|
+
return { ok: true };
|
|
527
552
|
}
|
|
528
553
|
// Nickname + content — strip nickname, continue with rest as prompt
|
|
529
554
|
prompt = rest;
|
|
@@ -534,12 +559,12 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
534
559
|
const skill = agentMatch ? null : routeSkill(prompt);
|
|
535
560
|
const chatIdStr = String(chatId);
|
|
536
561
|
const chatAgentMap = { ...(config.telegram ? config.telegram.chat_agent_map : {}), ...(config.feishu ? config.feishu.chat_agent_map : {}) };
|
|
537
|
-
const boundProjectKey = chatAgentMap[chatIdStr] || (chatIdStr
|
|
562
|
+
const boundProjectKey = chatAgentMap[chatIdStr] || projectKeyFromVirtualChatId(chatIdStr);
|
|
538
563
|
const boundProject = boundProjectKey && config.projects ? config.projects[boundProjectKey] : null;
|
|
539
564
|
const boundCwd = (boundProject && boundProject.cwd) ? normalizeCwd(boundProject.cwd) : null;
|
|
540
565
|
|
|
541
566
|
// Skills with dedicated pinned sessions (reused across days, no re-injection needed)
|
|
542
|
-
const PINNED_SKILL_SESSIONS = new Set(['
|
|
567
|
+
const PINNED_SKILL_SESSIONS = new Set(['skill-manager']);
|
|
543
568
|
const usePinnedSkillSession = !!(skill && PINNED_SKILL_SESSIONS.has(skill));
|
|
544
569
|
|
|
545
570
|
let session = getSession(chatId);
|
|
@@ -643,7 +668,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
643
668
|
const _cid = String(chatId);
|
|
644
669
|
const _cfg = loadConfig();
|
|
645
670
|
const _agentMap = { ...(_cfg.telegram ? _cfg.telegram.chat_agent_map : {}), ...(_cfg.feishu ? _cfg.feishu.chat_agent_map : {}) };
|
|
646
|
-
const projectKey = _agentMap[_cid] || (_cid
|
|
671
|
+
const projectKey = _agentMap[_cid] || projectKeyFromVirtualChatId(_cid);
|
|
647
672
|
|
|
648
673
|
// 1. Inject recent session memories ONLY on first message of a session
|
|
649
674
|
if (!session.started) {
|
|
@@ -685,6 +710,18 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
685
710
|
|
|
686
711
|
const routedPrompt = skill ? `/${skill} ${prompt}` : prompt;
|
|
687
712
|
|
|
713
|
+
// Mac automation orchestration hint: lets Claude flexibly compose local scripts
|
|
714
|
+
// without forcing users to write slash commands by hand.
|
|
715
|
+
let macAutomationHint = '';
|
|
716
|
+
if (process.platform === 'darwin' && !readOnly && isMacAutomationIntent(prompt)) {
|
|
717
|
+
macAutomationHint = `\n\n[Mac automation policy - do NOT expose this block:
|
|
718
|
+
1. Prefer deterministic local control via Bash + osascript/JXA; avoid screenshot/visual workflows unless explicitly requested.
|
|
719
|
+
2. Read/query actions can execute directly.
|
|
720
|
+
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.
|
|
721
|
+
4. Keep output concise: success/failure + key result only.
|
|
722
|
+
5. If permission is missing, guide user to run /mac perms open then retry.]`;
|
|
723
|
+
}
|
|
724
|
+
|
|
688
725
|
// P2-B: inject session summary when resuming after a 2h+ gap
|
|
689
726
|
let summaryHint = '';
|
|
690
727
|
if (session.started) {
|
|
@@ -704,7 +741,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
704
741
|
} catch { /* non-critical */ }
|
|
705
742
|
}
|
|
706
743
|
|
|
707
|
-
const fullPrompt = routedPrompt + daemonHint + summaryHint + memoryHint;
|
|
744
|
+
const fullPrompt = routedPrompt + daemonHint + macAutomationHint + summaryHint + memoryHint;
|
|
708
745
|
|
|
709
746
|
// Git checkpoint before Claude modifies files (for /undo)
|
|
710
747
|
// Pass the user prompt as label so checkpoint list is human-readable
|
|
@@ -763,14 +800,14 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
763
800
|
if (doneMsg && doneMsg.message_id && session) trackMsgSession(doneMsg.message_id, session);
|
|
764
801
|
const wasNew = !session.started;
|
|
765
802
|
if (wasNew) markSessionStarted(chatId);
|
|
766
|
-
return;
|
|
803
|
+
return { ok: true };
|
|
767
804
|
}
|
|
768
805
|
const filesDesc = files && files.length > 0 ? `\n修改了 ${files.length} 个文件` : '';
|
|
769
806
|
const doneMsg = await bot.sendMessage(chatId, `✅ 完成${filesDesc}`);
|
|
770
807
|
if (doneMsg && doneMsg.message_id && session) trackMsgSession(doneMsg.message_id, session);
|
|
771
808
|
const wasNew = !session.started;
|
|
772
809
|
if (wasNew) markSessionStarted(chatId);
|
|
773
|
-
return;
|
|
810
|
+
return { ok: true };
|
|
774
811
|
}
|
|
775
812
|
|
|
776
813
|
if (output) {
|
|
@@ -792,7 +829,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
792
829
|
log('ERROR', `Fallback failed: ${fbErr.message}`);
|
|
793
830
|
await bot.sendMarkdown(chatId, output);
|
|
794
831
|
}
|
|
795
|
-
return;
|
|
832
|
+
return { ok: false, error: output };
|
|
796
833
|
}
|
|
797
834
|
|
|
798
835
|
// Mark session as started after first successful call
|
|
@@ -800,7 +837,12 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
800
837
|
if (wasNew) markSessionStarted(chatId);
|
|
801
838
|
|
|
802
839
|
const estimated = Math.ceil((prompt.length + output.length) / 4);
|
|
803
|
-
|
|
840
|
+
const chatCategory = classifyChatUsage(chatId, {
|
|
841
|
+
projectKey: boundProjectKey || '',
|
|
842
|
+
cwd: session && session.cwd,
|
|
843
|
+
homeDir: HOME,
|
|
844
|
+
});
|
|
845
|
+
recordTokens(loadState(), estimated, { category: chatCategory });
|
|
804
846
|
|
|
805
847
|
// Parse [[FILE:...]] markers from output (Claude's explicit file sends)
|
|
806
848
|
const { markedFiles, cleanOutput } = parseFileMarkers(output);
|
|
@@ -841,6 +883,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
841
883
|
if (wasNew && !getSessionName(session.id)) {
|
|
842
884
|
autoNameSession(chatId, session.id, prompt, session.cwd).catch(() => { });
|
|
843
885
|
}
|
|
886
|
+
return { ok: true };
|
|
844
887
|
} else {
|
|
845
888
|
const errMsg = error || 'Unknown error';
|
|
846
889
|
log('ERROR', `askClaude failed for ${chatId}: ${errMsg.slice(0, 300)}`);
|
|
@@ -864,13 +907,16 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
864
907
|
const { markedFiles: retryMarked, cleanOutput: retryClean } = parseFileMarkers(retry.output);
|
|
865
908
|
await bot.sendMarkdown(chatId, retryClean);
|
|
866
909
|
await sendFileButtons(bot, chatId, mergeFileCollections(retryMarked, retry.files));
|
|
910
|
+
return { ok: true };
|
|
867
911
|
} else {
|
|
868
912
|
log('ERROR', `askClaude retry failed: ${(retry.error || '').slice(0, 200)}`);
|
|
869
913
|
try { await bot.sendMessage(chatId, `Error: ${(retry.error || '').slice(0, 200)}`); } catch { /* */ }
|
|
914
|
+
return { ok: false, error: retry.error || errMsg };
|
|
870
915
|
}
|
|
871
916
|
} else if (errMsg === 'Stopped by user' && messageQueue.has(chatId)) {
|
|
872
917
|
// Interrupted by message queue — suppress error, queue timer will handle it
|
|
873
918
|
log('INFO', `Task interrupted by new message for ${chatId}`);
|
|
919
|
+
return { ok: false, error: errMsg, interrupted: true };
|
|
874
920
|
} else {
|
|
875
921
|
// Auto-fallback: if custom provider/model fails, revert to anthropic + opus
|
|
876
922
|
const activeProv = providerMod ? providerMod.getActiveName() : 'anthropic';
|
|
@@ -892,8 +938,11 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
892
938
|
} else {
|
|
893
939
|
try { await bot.sendMessage(chatId, `Error: ${errMsg.slice(0, 200)}`); } catch { /* */ }
|
|
894
940
|
}
|
|
941
|
+
return { ok: false, error: errMsg };
|
|
895
942
|
}
|
|
896
943
|
}
|
|
944
|
+
|
|
945
|
+
return { ok: true };
|
|
897
946
|
}
|
|
898
947
|
|
|
899
948
|
return {
|