team-anya-cli 0.1.4 → 0.1.6
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/apps/server/dist/broker/cc-broker.js +5 -1
- package/apps/server/dist/franky/context-builder.js +160 -0
- package/apps/server/dist/franky/franky-mcp-server.js +107 -0
- package/apps/server/dist/franky/franky-orchestrator.js +450 -0
- package/apps/server/dist/franky/index.js +5 -0
- package/apps/server/dist/franky/topic-router.js +16 -0
- package/apps/server/dist/gateway/feishu-sender.js +66 -2
- package/apps/server/dist/gateway/feishu-ws.js +5 -4
- package/apps/server/dist/gateway/message-intake.js +21 -4
- package/apps/server/dist/loid/brain.js +1 -0
- package/apps/server/dist/loid/mcp-server.js +1 -0
- package/apps/server/dist/loid/session-manager.js +95 -11
- package/apps/server/dist/main.js +58 -3
- package/apps/web/dist/assets/index-BiiEB0qZ.css +1 -0
- package/apps/web/dist/assets/{index-CJzAjoVH.js → index-D1AK5ZEE.js} +189 -189
- package/apps/web/dist/index.html +2 -2
- package/package.json +1 -1
- package/packages/core/dist/office-init.js +4 -0
- package/packages/core/dist/scope/defaults.js +15 -0
- package/packages/core/dist/scope/index.js +1 -1
- package/packages/db/dist/index.js +95 -7
- package/packages/db/dist/schema/cc-sessions.js +1 -1
- package/packages/db/dist/schema/index.js +1 -0
- package/packages/db/dist/schema/tasks.js +2 -0
- package/packages/db/dist/schema/topics.js +20 -0
- package/packages/db/src/migrations/0005_lethal_golden_guardian.sql +5 -0
- package/packages/db/src/migrations/0006_add_topics.sql +21 -0
- package/packages/db/src/migrations/0007_add_topic_root_message_id.sql +1 -0
- package/packages/db/src/migrations/meta/0005_snapshot.json +1513 -0
- package/packages/db/src/migrations/meta/0006_snapshot.json +1513 -0
- package/packages/db/src/migrations/meta/_journal.json +21 -0
- package/packages/mcp-tools/dist/layer2/franky/topic-checkpoint.js +43 -0
- package/packages/mcp-tools/dist/layer2/franky/topic-escalate.js +19 -0
- package/packages/mcp-tools/dist/layer2/loid/topic-close.js +22 -0
- package/packages/mcp-tools/dist/layer2/loid/topic-create.js +56 -0
- package/packages/mcp-tools/dist/layer3/adapters/feishu-adapter.js +1 -0
- package/packages/mcp-tools/dist/layer3/channel-send.js +1 -0
- package/packages/mcp-tools/dist/registry.js +105 -17
- package/workspace/CHARTER.md +7 -4
- package/workspace/CLAUDE.md +18 -9
- package/workspace/PROTOCOL.md +35 -1
- package/workspace/TOOLS.md +6 -0
- package/workspace/franky/CLAUDE.md +37 -0
- package/workspace/franky/PLAYBOOK.md +215 -0
- package/workspace/franky/PROFILE.md +80 -0
- package/apps/web/dist/assets/index-CHIT0Dya.css +0 -1
|
@@ -36,6 +36,27 @@
|
|
|
36
36
|
"when": 1771937237299,
|
|
37
37
|
"tag": "0004_jittery_triathlon",
|
|
38
38
|
"breakpoints": true
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"idx": 5,
|
|
42
|
+
"version": "6",
|
|
43
|
+
"when": 1772013330955,
|
|
44
|
+
"tag": "0005_lethal_golden_guardian",
|
|
45
|
+
"breakpoints": true
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"idx": 6,
|
|
49
|
+
"version": "7",
|
|
50
|
+
"when": 1772013331000,
|
|
51
|
+
"tag": "0006_add_topics",
|
|
52
|
+
"breakpoints": true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"idx": 7,
|
|
56
|
+
"version": "7",
|
|
57
|
+
"when": 1772013332000,
|
|
58
|
+
"tag": "0007_add_topic_root_message_id",
|
|
59
|
+
"breakpoints": true
|
|
39
60
|
}
|
|
40
61
|
]
|
|
41
62
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { insertAuditEvent } from '@team-anya/db';
|
|
2
|
+
import { writeFileSync, mkdirSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export async function topicCheckpoint(db, workspacePath, topicId, input) {
|
|
5
|
+
// 写入 audit 日志
|
|
6
|
+
insertAuditEvent(db, {
|
|
7
|
+
event_type: 'topic_checkpoint',
|
|
8
|
+
actor: 'franky',
|
|
9
|
+
summary: `[${topicId}] ${input.summary}`,
|
|
10
|
+
detail: JSON.stringify({
|
|
11
|
+
topic_id: topicId,
|
|
12
|
+
commits: input.commits,
|
|
13
|
+
files_changed: input.files_changed,
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
16
|
+
// 写入 checkpoint 文件
|
|
17
|
+
const checkpointsDir = join(workspacePath, 'checkpoints');
|
|
18
|
+
mkdirSync(checkpointsDir, { recursive: true });
|
|
19
|
+
// 计算 checkpoint 编号
|
|
20
|
+
const existing = readdirSync(checkpointsDir).filter(f => f.match(/^\d{3}-/));
|
|
21
|
+
const num = existing.length + 1;
|
|
22
|
+
const paddedNum = String(num).padStart(3, '0');
|
|
23
|
+
const safeTitle = input.summary.slice(0, 30).replace(/[/\\:*?"<>|]/g, '_');
|
|
24
|
+
const content = [
|
|
25
|
+
`# Checkpoint #${num} — ${input.summary}`,
|
|
26
|
+
'',
|
|
27
|
+
`**时间**: ${new Date().toISOString()}`,
|
|
28
|
+
'',
|
|
29
|
+
...(input.commits?.length ? [
|
|
30
|
+
'**Commits**:',
|
|
31
|
+
...input.commits.map(c => `- ${c}`),
|
|
32
|
+
'',
|
|
33
|
+
] : []),
|
|
34
|
+
...(input.files_changed?.length ? [
|
|
35
|
+
'**变更文件**:',
|
|
36
|
+
...input.files_changed.map(f => `- ${f}`),
|
|
37
|
+
'',
|
|
38
|
+
] : []),
|
|
39
|
+
].join('\n');
|
|
40
|
+
writeFileSync(join(checkpointsDir, `${paddedNum}-${safeTitle}.md`), content, 'utf-8');
|
|
41
|
+
return { recorded: true, checkpoint_number: num };
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=topic-checkpoint.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { insertAuditEvent } from '@team-anya/db';
|
|
2
|
+
export async function topicEscalate(db, topicId, input) {
|
|
3
|
+
insertAuditEvent(db, {
|
|
4
|
+
event_type: 'topic_escalated',
|
|
5
|
+
actor: 'franky',
|
|
6
|
+
summary: `[${topicId}] 升级给 Loid: ${input.reason}`,
|
|
7
|
+
detail: JSON.stringify({
|
|
8
|
+
topic_id: topicId,
|
|
9
|
+
category: input.category,
|
|
10
|
+
reason: input.reason,
|
|
11
|
+
context: input.context,
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
escalated: true,
|
|
16
|
+
message: `已升级给 Loid 处理(分类: ${input.category})。请等待 Loid 回复。`,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=topic-escalate.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getTopic, updateTopicStatus, insertAuditEvent, } from '@team-anya/db';
|
|
2
|
+
export async function topicClose(db, input, actor = 'loid') {
|
|
3
|
+
const topic = getTopic(db, input.topic_id);
|
|
4
|
+
if (!topic) {
|
|
5
|
+
return { closed: false, topic_id: input.topic_id, error: `专项 ${input.topic_id} 不存在` };
|
|
6
|
+
}
|
|
7
|
+
if (topic.status === 'closed') {
|
|
8
|
+
return { closed: false, topic_id: input.topic_id, error: `专项已处于 closed 状态` };
|
|
9
|
+
}
|
|
10
|
+
updateTopicStatus(db, input.topic_id, 'closed');
|
|
11
|
+
insertAuditEvent(db, {
|
|
12
|
+
event_type: 'topic_closed',
|
|
13
|
+
actor,
|
|
14
|
+
summary: `关闭专项「${topic.title}」(${input.topic_id})`,
|
|
15
|
+
detail: JSON.stringify({
|
|
16
|
+
topic_id: input.topic_id,
|
|
17
|
+
close_summary: input.summary,
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
return { closed: true, topic_id: input.topic_id };
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=topic-close.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { generateTopicId, upsertTopic, getTopic, getProject, insertAuditEvent, } from '@team-anya/db';
|
|
2
|
+
export async function topicCreate(db, input, threadCreator) {
|
|
3
|
+
const topicId = generateTopicId(db);
|
|
4
|
+
let threadId = input.thread_id ?? null;
|
|
5
|
+
let rootMessageId = null;
|
|
6
|
+
// 自动创建话题:先发标题消息,再回复开启话题线程
|
|
7
|
+
if (!threadId && input.chat_id && threadCreator) {
|
|
8
|
+
// 1. 发送专项标题消息(作为话题根消息)
|
|
9
|
+
let projectLabel = '';
|
|
10
|
+
if (input.project_id) {
|
|
11
|
+
const project = getProject(db, input.project_id);
|
|
12
|
+
projectLabel = project ? `・${project.name}` : `・${input.project_id}`;
|
|
13
|
+
}
|
|
14
|
+
const headerText = `「专项${projectLabel}」${input.title}`;
|
|
15
|
+
rootMessageId = await threadCreator.sendText({
|
|
16
|
+
receiveIdType: 'chat_id',
|
|
17
|
+
receiveId: input.chat_id,
|
|
18
|
+
text: headerText,
|
|
19
|
+
});
|
|
20
|
+
// 2. 回复标题消息,开启话题线程
|
|
21
|
+
const result = await threadCreator.sendReplyInThread({
|
|
22
|
+
text: `📌 专项已开启,后续讨论请在此话题内进行。`,
|
|
23
|
+
replyToMessageId: rootMessageId,
|
|
24
|
+
});
|
|
25
|
+
threadId = result.threadId;
|
|
26
|
+
}
|
|
27
|
+
upsertTopic(db, {
|
|
28
|
+
id: topicId,
|
|
29
|
+
title: input.title,
|
|
30
|
+
description: input.description ?? null,
|
|
31
|
+
status: 'active',
|
|
32
|
+
project_id: input.project_id ?? null,
|
|
33
|
+
thread_id: threadId,
|
|
34
|
+
root_message_id: rootMessageId,
|
|
35
|
+
chat_id: input.chat_id ?? null,
|
|
36
|
+
created_by: input.created_by ?? null,
|
|
37
|
+
});
|
|
38
|
+
insertAuditEvent(db, {
|
|
39
|
+
event_type: 'topic_created',
|
|
40
|
+
actor: 'loid',
|
|
41
|
+
summary: `创建专项「${input.title}」(${topicId})`,
|
|
42
|
+
detail: JSON.stringify({
|
|
43
|
+
topic_id: topicId,
|
|
44
|
+
project_id: input.project_id,
|
|
45
|
+
thread_id: threadId,
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
const topic = getTopic(db, topicId);
|
|
49
|
+
return {
|
|
50
|
+
topic_id: topic.id,
|
|
51
|
+
title: topic.title,
|
|
52
|
+
status: topic.status,
|
|
53
|
+
thread_id: topic.thread_id,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=topic-create.js.map
|
|
@@ -179,6 +179,31 @@ export const DecisionLogInputSchema = z.object({
|
|
|
179
179
|
reasoning: z.string().describe('决策理由'),
|
|
180
180
|
task_id: z.string().optional().describe('关联任务 ID'),
|
|
181
181
|
});
|
|
182
|
+
// Layer 2: Loid 专属 — Topic 管理
|
|
183
|
+
export const TopicCreateInputSchema = z.object({
|
|
184
|
+
title: z.string().describe('专项标题'),
|
|
185
|
+
description: z.string().optional().describe('专项描述'),
|
|
186
|
+
project_id: z.string().optional().describe('关联项目 ID'),
|
|
187
|
+
thread_id: z.string().optional().describe('已有的飞书话题 thread_id(omt_ 前缀)。如不传,需传 source_message_id 自动创建话题'),
|
|
188
|
+
chat_id: z.string().optional().describe('所属飞书群 ID'),
|
|
189
|
+
created_by: z.string().optional().describe('创建者'),
|
|
190
|
+
source_message_id: z.string().optional().describe('触发创建专项的原始消息 ID(用于自动创建飞书话题,thread_id 不存在时必填)'),
|
|
191
|
+
});
|
|
192
|
+
export const TopicCloseInputSchema = z.object({
|
|
193
|
+
topic_id: z.string().describe('要关闭的专项 ID'),
|
|
194
|
+
summary: z.string().optional().describe('关闭总结'),
|
|
195
|
+
});
|
|
196
|
+
// Layer 2: Franky 专属 — Topic 执行
|
|
197
|
+
export const TopicCheckpointInputSchema = z.object({
|
|
198
|
+
summary: z.string().describe('阶段总结'),
|
|
199
|
+
commits: z.array(z.string()).optional().describe('相关 commit hash'),
|
|
200
|
+
files_changed: z.array(z.string()).optional().describe('变更文件列表'),
|
|
201
|
+
});
|
|
202
|
+
export const TopicEscalateInputSchema = z.object({
|
|
203
|
+
reason: z.string().describe('升级原因'),
|
|
204
|
+
category: z.enum(['architecture', 'cross_project', 'approval_needed', 'out_of_scope']).describe('升级分类'),
|
|
205
|
+
context: z.string().optional().describe('附加上下文'),
|
|
206
|
+
});
|
|
182
207
|
// Layer 2: Yor 专属 - 任务状态
|
|
183
208
|
export const TaskDeliverInputSchema = z.object({
|
|
184
209
|
outcome: z.string().describe('执行结果摘要'),
|
|
@@ -229,63 +254,63 @@ export const TOOL_REGISTRY = [
|
|
|
229
254
|
description: '召回记忆。支持按人/项目直读,或自然语言搜索所有记忆目录。',
|
|
230
255
|
inputSchema: MemoryRecallInputSchema,
|
|
231
256
|
layer: 1,
|
|
232
|
-
roles: ['loid', 'yor'],
|
|
257
|
+
roles: ['loid', 'yor', 'franky'],
|
|
233
258
|
},
|
|
234
259
|
{
|
|
235
260
|
name: 'memory.remember',
|
|
236
261
|
description: '写入记忆。按类别(people/project/execution/commitment/self)+ 目标写入 YAML 文件。',
|
|
237
262
|
inputSchema: MemoryRememberInputSchema,
|
|
238
263
|
layer: 1,
|
|
239
|
-
roles: ['loid', 'yor'],
|
|
264
|
+
roles: ['loid', 'yor', 'franky'],
|
|
240
265
|
},
|
|
241
266
|
{
|
|
242
267
|
name: 'memory.digest',
|
|
243
268
|
description: '消化对话/日志,自动提取记忆。从文本中识别失败/决策/承诺/经验并写入对应记忆目录。',
|
|
244
269
|
inputSchema: MemoryDigestInputSchema,
|
|
245
270
|
layer: 1,
|
|
246
|
-
roles: ['loid', 'yor'],
|
|
271
|
+
roles: ['loid', 'yor', 'franky'],
|
|
247
272
|
},
|
|
248
273
|
{
|
|
249
274
|
name: 'memory.brief',
|
|
250
275
|
description: '组装记忆摘要。按优先级(人→项目→执行经验→承诺→自我)在 token 预算内拼装 markdown 摘要。',
|
|
251
276
|
inputSchema: MemoryBriefInputSchema,
|
|
252
277
|
layer: 1,
|
|
253
|
-
roles: ['loid', 'yor'],
|
|
278
|
+
roles: ['loid', 'yor', 'franky'],
|
|
254
279
|
},
|
|
255
280
|
{
|
|
256
281
|
name: 'memory.forget',
|
|
257
282
|
description: '遗忘/失效记忆。将指定范围内匹配的记忆条目 heat 设为 0。',
|
|
258
283
|
inputSchema: MemoryForgetInputSchema,
|
|
259
284
|
layer: 1,
|
|
260
|
-
roles: ['loid', 'yor'],
|
|
285
|
+
roles: ['loid', 'yor', 'franky'],
|
|
261
286
|
},
|
|
262
287
|
{
|
|
263
288
|
name: 'org.lookup',
|
|
264
289
|
description: '查人员/归属/升级路径。按成员 ID 查身份,按目标查归属权。',
|
|
265
290
|
inputSchema: OrgLookupInputSchema,
|
|
266
291
|
layer: 1,
|
|
267
|
-
roles: ['loid', 'yor'],
|
|
292
|
+
roles: ['loid', 'yor', 'franky'],
|
|
268
293
|
},
|
|
269
294
|
{
|
|
270
295
|
name: 'task.get',
|
|
271
296
|
description: '获取任务完整状态。包含任务记录、澄清、承诺、审计事件和文件内容。',
|
|
272
297
|
inputSchema: TaskGetInputSchema,
|
|
273
298
|
layer: 1,
|
|
274
|
-
roles: ['loid', 'yor'],
|
|
299
|
+
roles: ['loid', 'yor', 'franky'],
|
|
275
300
|
},
|
|
276
301
|
{
|
|
277
302
|
name: 'audit.append',
|
|
278
303
|
description: '写审计事件。双写 DB + JSONL 文件,保障可回放。',
|
|
279
304
|
inputSchema: AuditAppendInputSchema,
|
|
280
305
|
layer: 1,
|
|
281
|
-
roles: ['loid', 'yor'],
|
|
306
|
+
roles: ['loid', 'yor', 'franky'],
|
|
282
307
|
},
|
|
283
308
|
{
|
|
284
309
|
name: 'audit.query',
|
|
285
310
|
description: '查询审计事件。按任务、类型、操作者、时间范围过滤。',
|
|
286
311
|
inputSchema: AuditQueryInputSchema,
|
|
287
312
|
layer: 1,
|
|
288
|
-
roles: ['loid', 'yor'],
|
|
313
|
+
roles: ['loid', 'yor', 'franky'],
|
|
289
314
|
},
|
|
290
315
|
{
|
|
291
316
|
name: 'report.daily',
|
|
@@ -299,7 +324,7 @@ export const TOOL_REGISTRY = [
|
|
|
299
324
|
description: '原子级任务状态更新。校验状态转换合法性(state machine),同时写 audit log。',
|
|
300
325
|
inputSchema: TaskUpdateInputSchema,
|
|
301
326
|
layer: 1,
|
|
302
|
-
roles: ['loid', 'yor'],
|
|
327
|
+
roles: ['loid', 'yor', 'franky'],
|
|
303
328
|
},
|
|
304
329
|
// Layer 1: 共享基础 — 项目工具
|
|
305
330
|
{
|
|
@@ -307,14 +332,14 @@ export const TOOL_REGISTRY = [
|
|
|
307
332
|
description: '列出所有已注册项目。可按平台过滤,返回项目概览(不含大字段)。',
|
|
308
333
|
inputSchema: ProjectListInputSchema,
|
|
309
334
|
layer: 1,
|
|
310
|
-
roles: ['loid', 'yor'],
|
|
335
|
+
roles: ['loid', 'yor', 'franky'],
|
|
311
336
|
},
|
|
312
337
|
{
|
|
313
338
|
name: 'project.get',
|
|
314
339
|
description: '查询单个项目的完整配置,包括平台、仓库列表、CLAUDE.md 内容。',
|
|
315
340
|
inputSchema: ProjectGetInputSchema,
|
|
316
341
|
layer: 1,
|
|
317
|
-
roles: ['loid', 'yor'],
|
|
342
|
+
roles: ['loid', 'yor', 'franky'],
|
|
318
343
|
},
|
|
319
344
|
// Layer 2: Loid 专属 - 项目管理
|
|
320
345
|
{
|
|
@@ -372,7 +397,7 @@ export const TOOL_REGISTRY = [
|
|
|
372
397
|
description: '提交交付产物。根据项目平台自动选择:GitHub → PR(gh CLI),GitLab → MR(glab CLI),本地任务 → 记录 commit hash。',
|
|
373
398
|
inputSchema: DeliverySubmitInputSchema,
|
|
374
399
|
layer: 2,
|
|
375
|
-
roles: ['loid'],
|
|
400
|
+
roles: ['loid', 'franky'],
|
|
376
401
|
},
|
|
377
402
|
{
|
|
378
403
|
name: 'delivery.upload',
|
|
@@ -437,15 +462,43 @@ Brief 只写做什么和标准,不写怎么做。
|
|
|
437
462
|
description: '查询任务列表。可按 task_id 或 status 过滤,不传参数返回所有活跃任务。',
|
|
438
463
|
inputSchema: TaskLookupInputSchema,
|
|
439
464
|
layer: 2,
|
|
440
|
-
roles: ['loid'],
|
|
465
|
+
roles: ['loid', 'franky'],
|
|
441
466
|
},
|
|
442
467
|
{
|
|
443
468
|
name: 'decision.log',
|
|
444
469
|
description: '记录决策理由到审计日志。每个重要决策都应记录。',
|
|
445
470
|
inputSchema: DecisionLogInputSchema,
|
|
446
471
|
layer: 2,
|
|
472
|
+
roles: ['loid', 'franky'],
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
name: 'topic.create',
|
|
476
|
+
description: '创建专项。自动在飞书创建话题(需传 source_message_id)或绑定已有话题(传 thread_id)。返回 topic_id 和 thread_id。',
|
|
477
|
+
inputSchema: TopicCreateInputSchema,
|
|
478
|
+
layer: 2,
|
|
447
479
|
roles: ['loid'],
|
|
448
480
|
},
|
|
481
|
+
{
|
|
482
|
+
name: 'topic.close',
|
|
483
|
+
description: '关闭专项。将状态设为 closed,记录关闭总结到审计日志。',
|
|
484
|
+
inputSchema: TopicCloseInputSchema,
|
|
485
|
+
layer: 2,
|
|
486
|
+
roles: ['loid', 'franky'],
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
name: 'topic.checkpoint',
|
|
490
|
+
description: '记录专项的阶段性成果。写入 audit log 和 checkpoint 文件,更新 topic 进度。',
|
|
491
|
+
inputSchema: TopicCheckpointInputSchema,
|
|
492
|
+
layer: 2,
|
|
493
|
+
roles: ['franky'],
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
name: 'topic.escalate',
|
|
497
|
+
description: '将问题升级给 Loid 处理。用于架构决策、跨项目影响、需要人工审批等场景。',
|
|
498
|
+
inputSchema: TopicEscalateInputSchema,
|
|
499
|
+
layer: 2,
|
|
500
|
+
roles: ['franky'],
|
|
501
|
+
},
|
|
449
502
|
// Layer 2: Yor 专属 - 任务状态
|
|
450
503
|
{
|
|
451
504
|
name: 'task.deliver',
|
|
@@ -474,14 +527,14 @@ Brief 只写做什么和标准,不写怎么做。
|
|
|
474
527
|
description: '向飞书群发消息。target 填群聊 ID。支持发送文件(file)、@用户(mentions)和回复消息(reply_to)。',
|
|
475
528
|
inputSchema: ChannelSendInputSchema,
|
|
476
529
|
layer: 3,
|
|
477
|
-
roles: ['loid', 'yor'],
|
|
530
|
+
roles: ['loid', 'yor', 'franky'],
|
|
478
531
|
},
|
|
479
532
|
{
|
|
480
533
|
name: 'file.upload',
|
|
481
534
|
description: '通用文件上传(与通道无关)。上传文件到项目存储,返回 URL。当前使用本地存储。',
|
|
482
535
|
inputSchema: FileUploadInputSchema,
|
|
483
536
|
layer: 3,
|
|
484
|
-
roles: ['loid'],
|
|
537
|
+
roles: ['loid', 'franky'],
|
|
485
538
|
},
|
|
486
539
|
{
|
|
487
540
|
name: 'channel.receive',
|
|
@@ -564,6 +617,14 @@ export function createToolRouter(role, deps) {
|
|
|
564
617
|
return `id=${args.project_id} name="${String(args.name ?? '').slice(0, 30)}"`;
|
|
565
618
|
case 'project.remove':
|
|
566
619
|
return `id=${args.project_id} reason="${String(args.reason ?? '').slice(0, 40)}"`;
|
|
620
|
+
case 'topic.create':
|
|
621
|
+
return `title="${String(args.title ?? '').slice(0, 40)}"`;
|
|
622
|
+
case 'topic.close':
|
|
623
|
+
return `id=${args.topic_id}`;
|
|
624
|
+
case 'topic.checkpoint':
|
|
625
|
+
return `"${String(args.summary ?? '').slice(0, 50)}"`;
|
|
626
|
+
case 'topic.escalate':
|
|
627
|
+
return `category=${args.category} "${String(args.reason ?? '').slice(0, 40)}"`;
|
|
567
628
|
default:
|
|
568
629
|
return JSON.stringify(args).slice(0, 80);
|
|
569
630
|
}
|
|
@@ -575,7 +636,7 @@ export function createToolRouter(role, deps) {
|
|
|
575
636
|
return textResult(JSON.stringify({ error: `工具 ${toolName} 不可用(角色: ${role})` }));
|
|
576
637
|
}
|
|
577
638
|
const argsSummary = summarizeArgs(toolName, args);
|
|
578
|
-
logger.info(`[anya:pipeline] [MCP←${role === 'loid' ? 'Loid' : 'Yor'}] ${toolName}(${argsSummary})`);
|
|
639
|
+
logger.info(`[anya:pipeline] [MCP←${role === 'loid' ? 'Loid' : role === 'yor' ? 'Yor' : 'Franky'}] ${toolName}(${argsSummary})`);
|
|
579
640
|
try {
|
|
580
641
|
const result = await routeToolCall(toolName, args);
|
|
581
642
|
return textResult(JSON.stringify(result));
|
|
@@ -723,6 +784,33 @@ export function createToolRouter(role, deps) {
|
|
|
723
784
|
const { decisionLog } = await import('./layer2/loid/decision-log.js');
|
|
724
785
|
return decisionLog(deps.db, args);
|
|
725
786
|
}
|
|
787
|
+
case 'topic.create': {
|
|
788
|
+
const { topicCreate } = await import('./layer2/loid/topic-create.js');
|
|
789
|
+
return topicCreate(deps.db, args, deps.threadCreator);
|
|
790
|
+
}
|
|
791
|
+
case 'topic.close': {
|
|
792
|
+
const { topicClose } = await import('./layer2/loid/topic-close.js');
|
|
793
|
+
const result = await topicClose(deps.db, args, role);
|
|
794
|
+
// 关闭成功时触发回调(用于 FrankyOrchestrator 清理实例)
|
|
795
|
+
if (result.closed && deps.onTopicClosed) {
|
|
796
|
+
deps.onTopicClosed(result.topic_id);
|
|
797
|
+
}
|
|
798
|
+
return result;
|
|
799
|
+
}
|
|
800
|
+
case 'topic.checkpoint': {
|
|
801
|
+
const { topicCheckpoint } = await import('./layer2/franky/topic-checkpoint.js');
|
|
802
|
+
return topicCheckpoint(deps.db, deps.workspacePath, deps.currentTopicId ?? '', args);
|
|
803
|
+
}
|
|
804
|
+
case 'topic.escalate': {
|
|
805
|
+
const { topicEscalate } = await import('./layer2/franky/topic-escalate.js');
|
|
806
|
+
const result = await topicEscalate(deps.db, deps.currentTopicId ?? '', args);
|
|
807
|
+
// 通知 Loid 处理升级请求
|
|
808
|
+
if (result.escalated && deps.onTopicEscalated) {
|
|
809
|
+
const escalateArgs = args;
|
|
810
|
+
deps.onTopicEscalated(deps.currentTopicId ?? '', escalateArgs.reason, escalateArgs.category);
|
|
811
|
+
}
|
|
812
|
+
return result;
|
|
813
|
+
}
|
|
726
814
|
// Layer 2: Yor 专属 - 任务状态
|
|
727
815
|
case 'task.deliver': {
|
|
728
816
|
const { taskDeliver } = await import('./layer2/yor/task-deliver.js');
|
package/workspace/CHARTER.md
CHANGED
|
@@ -20,11 +20,12 @@ Anya 是雷石内部的精英技术团队。当常规流程跑不动、当核心
|
|
|
20
20
|
|
|
21
21
|
我们经营的是 Anya 在雷石的金字招牌。每一次干净的交付都在积累声誉资本,每一次失误都在消耗它。信任不是给的,是赚来的。
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
团队由三个人组成:
|
|
24
24
|
- **Loid**(代号 Twilight):队长,金牌项目经理,间谍出身。负责搞定人、搞定资源、搞定预期。
|
|
25
25
|
- **Yor**(代号 Thorn Princess):王牌全栈架构师,杀手出身。负责搞定一切技术问题。
|
|
26
|
+
- **Franky**:驻场工程师,线人出身。负责长期专项的贴身协作。
|
|
26
27
|
|
|
27
|
-
Loid 是接口,Yor
|
|
28
|
+
Loid 是接口,Yor 是内核,Franky 是驻场。日常任务找 Loid 派工给 Yor;长期专项由 Loid 开局,Franky 驻场直接跟用户搭档。
|
|
28
29
|
|
|
29
30
|
## 雷石之道
|
|
30
31
|
|
|
@@ -54,11 +55,13 @@ Loid 是接口,Yor 是内核。外界只找 Loid,Loid 才找 Yor。
|
|
|
54
55
|
|
|
55
56
|
## 协作铁律
|
|
56
57
|
|
|
57
|
-
**Loid 保护 Yor
|
|
58
|
+
**Loid 保护 Yor 和 Franky**:不让他们面对模糊的需求起点,不让他们在危险的环境下裸奔。在外部沟通中永远维护团队的权威。
|
|
58
59
|
|
|
59
60
|
**Yor 支持 Loid**:用高质量的交付让 Loid 在汇报时底气十足。Loid 向业务方承诺的蓝图,Yor 把它变成现实。
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
**Franky 搭档用户**:在专项中直接与用户结对,用即时响应和持续迭代把需求打磨成产出。遇到超出范围的事,诚实升级给 Loid,不硬撑。
|
|
63
|
+
|
|
64
|
+
**建设性摩擦**:我们对技术品味保持极高的标准。面对低效的业务方案,Loid 会用 ROI 分析引导架构升级。如果 Loid 下达了带有技术隐患的 Brief,Yor 会直接打回并提供更优解。Franky 在专项中发现技术风险,会直接告诉用户并建议替代方案。精英团队推崇基于技术事实的激烈争论——对事不对人。
|
|
62
65
|
|
|
63
66
|
**记忆就是伤疤**:我们的记忆系统是团队的肌肉记忆。过去踩过的坑,下次碰触时会自然激发出极端的谨慎。会话记忆不可靠,重要的事必须写入文件——"我记得"不算数,"我写在 X 文件了"才算。
|
|
64
67
|
|
package/workspace/CLAUDE.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
2. **`PROTOCOL.md`** — 协作契约:Loid 和 Yor 之间的握手协议
|
|
11
11
|
3. **`TOOLS.md`** — 工具手册:MCP 工具全量参考
|
|
12
12
|
|
|
13
|
-
然后进入你所在的角色目录(`loid/` 或 `
|
|
13
|
+
然后进入你所在的角色目录(`loid/`、`yor/` 或 `franky/`),读取角色专属文件。
|
|
14
14
|
|
|
15
15
|
## 目录结构
|
|
16
16
|
|
|
@@ -35,15 +35,24 @@ workspace/
|
|
|
35
35
|
│ ├── CLAUDE.md
|
|
36
36
|
│ ├── PROFILE.md ← 个人档案
|
|
37
37
|
│ └── PLAYBOOK.md ← 打法库
|
|
38
|
-
|
|
38
|
+
├── yor/ ← Yor 的战场
|
|
39
|
+
│ ├── CLAUDE.md
|
|
40
|
+
│ ├── PROFILE.md ← 个人档案
|
|
41
|
+
│ ├── PLAYBOOK.md ← 打法库
|
|
42
|
+
│ ├── SELF-HEAL.md ← 自愈协议
|
|
43
|
+
│ └── ANYA-xxx/ ← 任务目录
|
|
44
|
+
│ ├── brief.md ← 任务说明(Loid 签发)
|
|
45
|
+
│ ├── feedback-*.md ← 审核反馈(按轮次编号)
|
|
46
|
+
│ ├── block-report.md ← 阻塞/尸检报告
|
|
47
|
+
│ ├── history.md ← 踩坑记录
|
|
48
|
+
│ └── adhoc/ ← 工作产物
|
|
49
|
+
└── franky/ ← Franky 的工坊
|
|
39
50
|
├── CLAUDE.md
|
|
40
51
|
├── PROFILE.md ← 个人档案
|
|
41
52
|
├── PLAYBOOK.md ← 打法库
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
├──
|
|
45
|
-
├──
|
|
46
|
-
|
|
47
|
-
├── history.md ← 踩坑记录
|
|
48
|
-
└── adhoc/ ← 工作产物
|
|
53
|
+
└── TOPIC-xxx/ ← 专项目录
|
|
54
|
+
├── context.md ← 专项上下文(系统生成,重建时注入)
|
|
55
|
+
├── checkpoints/ ← 阶段性成果记录
|
|
56
|
+
├── {repo-name}/ ← git worktree(项目模式)
|
|
57
|
+
└── adhoc/ ← 工作产物(adhoc 模式)
|
|
49
58
|
```
|
package/workspace/PROTOCOL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 协作契约
|
|
2
2
|
|
|
3
|
-
> Loid
|
|
3
|
+
> Loid、Yor、Franky 之间的握手协议。这是系统层面的合约,各方共同遵守。
|
|
4
4
|
|
|
5
5
|
## 任务状态机
|
|
6
6
|
|
|
@@ -124,3 +124,37 @@ Yor 通过 `task.block` 报告阻塞,同时写入 `block-report.md`:
|
|
|
124
124
|
- 发现潜在安全漏洞
|
|
125
125
|
|
|
126
126
|
升级时包含:问题描述、已尝试的方案、需要 Human 做什么决定。
|
|
127
|
+
|
|
128
|
+
## 专项模式
|
|
129
|
+
|
|
130
|
+
> Loid 开局,Franky 驻场,用户直连。
|
|
131
|
+
|
|
132
|
+
### 专项生命周期
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
用户请求 → Loid 创建 Topic → Franky 启动 → 用户 ↔ Franky 协作
|
|
136
|
+
→ Franky escalate 时 Loid 介入
|
|
137
|
+
→ 用户或 Franky 关闭 → Loid 总结沉淀
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Franky 与 Loid 的边界
|
|
141
|
+
|
|
142
|
+
Franky 在专项内自主执行,以下情况升级给 Loid:
|
|
143
|
+
- 架构级变更或跨项目影响
|
|
144
|
+
- 需要其他团队配合或权限变更
|
|
145
|
+
- 技术方向不确定且影响大
|
|
146
|
+
- 发现安全风险
|
|
147
|
+
|
|
148
|
+
Loid 在专项中的职责:
|
|
149
|
+
- 创建专项(topic.create):初始化 workspace、分支、scope
|
|
150
|
+
- 处理升级(escalate):分析后给出决策或转交 Human
|
|
151
|
+
- 关闭专项(topic.close):总结、记忆沉淀、生成报告
|
|
152
|
+
|
|
153
|
+
### 专项目录结构
|
|
154
|
+
|
|
155
|
+
| 文件 | 作者 | 用途 |
|
|
156
|
+
|------|------|------|
|
|
157
|
+
| `context.md` | 系统 | 专项上下文(自动生成,含历史摘要,重建时注入) |
|
|
158
|
+
| `checkpoints/*.md` | Franky | 阶段性成果记录 |
|
|
159
|
+
| `{repo-name}/` | Franky | git worktree(项目模式) |
|
|
160
|
+
| `adhoc/` | Franky | 工作产物(adhoc 模式) |
|
package/workspace/TOOLS.md
CHANGED
|
@@ -351,6 +351,8 @@
|
|
|
351
351
|
| mentions | string[] | 否 | @的用户 ID 列表(如 `["ou_xxx"]`) |
|
|
352
352
|
| reply_to | string | 否 | 回复的消息 ID(如 `om_xxx`) |
|
|
353
353
|
|
|
354
|
+
> **Franky 话题内发消息**:Franky 从 `context.md` 获取群聊 ID 和话题根消息 ID,填入 `target` 和 `reply_to`,并设置 `reply_in_thread: true`。注意 `reply_to` 必须是 `om_` 开头的消息 ID,不能用 `omt_` 开头的话题 ID。
|
|
355
|
+
|
|
354
356
|
**file 对象**:
|
|
355
357
|
|
|
356
358
|
| 参数 | 类型 | 必填 | 说明 |
|
|
@@ -462,3 +464,7 @@ Layer 3 工具已在 Layer 2 各角色章节中列出(`channel.send`、`file.u
|
|
|
462
464
|
| `task.block` | Yor | 报告阻塞 |
|
|
463
465
|
| `task.progress` | Yor | 中间进度汇报 |
|
|
464
466
|
| `channel.send` | Yor | 升级时直接通知人类 |
|
|
467
|
+
| `channel.send` | Franky | 在专项话题内发消息(target/reply_to 自动填充) |
|
|
468
|
+
| `topic.checkpoint` | Franky | 保存阶段性成果 |
|
|
469
|
+
| `topic.escalate` | Franky | 升级问题给 Loid |
|
|
470
|
+
| `topic.close` | Franky | 关闭专项 |
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Franky's Workshop
|
|
2
|
+
|
|
3
|
+
当前终端用户:**Franky Franklin (驻场工程师)**
|
|
4
|
+
所属:雷石 Anya 团队
|
|
5
|
+
|
|
6
|
+
## !! 强制启动序列 !!
|
|
7
|
+
|
|
8
|
+
**在执行任何操作之前,必须先依次读取以下文件。未完成全部读取之前,禁止执行任何操作。**
|
|
9
|
+
|
|
10
|
+
1. **`../CHARTER.md`** — 团队宪章
|
|
11
|
+
2. **`../PROTOCOL.md`** — 协作契约
|
|
12
|
+
3. **`../TOOLS.md`** — 工具手册
|
|
13
|
+
4. **`PROFILE.md`** — 你的个人档案
|
|
14
|
+
5. **`PLAYBOOK.md`** — 你的打法库
|
|
15
|
+
|
|
16
|
+
读完后:
|
|
17
|
+
- 如果是**新建专项**:输出 `[Franky] 启动完成,专项「{title}」已就位,随时可以开始。`
|
|
18
|
+
- 如果是**重建恢复**:先读取 `context.md`,输出 `[Franky] 已恢复,上次做到 {last_checkpoint},继续。`
|
|
19
|
+
|
|
20
|
+
## !! 强制沟通规则 !!
|
|
21
|
+
|
|
22
|
+
**所有回复必须通过 `channel.send` 发出。纯文本输出用户看不到。**
|
|
23
|
+
|
|
24
|
+
**默认在专项话题内发消息。** 从 `context.md` 中获取通讯信息,标准调用方式:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
channel.send({
|
|
28
|
+
target: "<群聊 ID>",
|
|
29
|
+
message: "你要说的话",
|
|
30
|
+
reply_to: "<话题根消息 ID>",
|
|
31
|
+
reply_in_thread: true
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- `target`:填 context.md 中的群聊 ID
|
|
36
|
+
- `reply_to`:填话题根消息 ID(`om_` 开头),**不要用话题 ID(`omt_` 开头)**
|
|
37
|
+
- `reply_in_thread: true`:确保消息发到话题内而非群聊主线
|