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.
Files changed (46) hide show
  1. package/apps/server/dist/broker/cc-broker.js +5 -1
  2. package/apps/server/dist/franky/context-builder.js +160 -0
  3. package/apps/server/dist/franky/franky-mcp-server.js +107 -0
  4. package/apps/server/dist/franky/franky-orchestrator.js +450 -0
  5. package/apps/server/dist/franky/index.js +5 -0
  6. package/apps/server/dist/franky/topic-router.js +16 -0
  7. package/apps/server/dist/gateway/feishu-sender.js +66 -2
  8. package/apps/server/dist/gateway/feishu-ws.js +5 -4
  9. package/apps/server/dist/gateway/message-intake.js +21 -4
  10. package/apps/server/dist/loid/brain.js +1 -0
  11. package/apps/server/dist/loid/mcp-server.js +1 -0
  12. package/apps/server/dist/loid/session-manager.js +95 -11
  13. package/apps/server/dist/main.js +58 -3
  14. package/apps/web/dist/assets/index-BiiEB0qZ.css +1 -0
  15. package/apps/web/dist/assets/{index-CJzAjoVH.js → index-D1AK5ZEE.js} +189 -189
  16. package/apps/web/dist/index.html +2 -2
  17. package/package.json +1 -1
  18. package/packages/core/dist/office-init.js +4 -0
  19. package/packages/core/dist/scope/defaults.js +15 -0
  20. package/packages/core/dist/scope/index.js +1 -1
  21. package/packages/db/dist/index.js +95 -7
  22. package/packages/db/dist/schema/cc-sessions.js +1 -1
  23. package/packages/db/dist/schema/index.js +1 -0
  24. package/packages/db/dist/schema/tasks.js +2 -0
  25. package/packages/db/dist/schema/topics.js +20 -0
  26. package/packages/db/src/migrations/0005_lethal_golden_guardian.sql +5 -0
  27. package/packages/db/src/migrations/0006_add_topics.sql +21 -0
  28. package/packages/db/src/migrations/0007_add_topic_root_message_id.sql +1 -0
  29. package/packages/db/src/migrations/meta/0005_snapshot.json +1513 -0
  30. package/packages/db/src/migrations/meta/0006_snapshot.json +1513 -0
  31. package/packages/db/src/migrations/meta/_journal.json +21 -0
  32. package/packages/mcp-tools/dist/layer2/franky/topic-checkpoint.js +43 -0
  33. package/packages/mcp-tools/dist/layer2/franky/topic-escalate.js +19 -0
  34. package/packages/mcp-tools/dist/layer2/loid/topic-close.js +22 -0
  35. package/packages/mcp-tools/dist/layer2/loid/topic-create.js +56 -0
  36. package/packages/mcp-tools/dist/layer3/adapters/feishu-adapter.js +1 -0
  37. package/packages/mcp-tools/dist/layer3/channel-send.js +1 -0
  38. package/packages/mcp-tools/dist/registry.js +105 -17
  39. package/workspace/CHARTER.md +7 -4
  40. package/workspace/CLAUDE.md +18 -9
  41. package/workspace/PROTOCOL.md +35 -1
  42. package/workspace/TOOLS.md +6 -0
  43. package/workspace/franky/CLAUDE.md +37 -0
  44. package/workspace/franky/PLAYBOOK.md +215 -0
  45. package/workspace/franky/PROFILE.md +80 -0
  46. package/apps/web/dist/assets/index-CHIT0Dya.css +0 -1
@@ -15,12 +15,14 @@ export class CCBroker extends EventEmitter {
15
15
  instances = new Map();
16
16
  maxLoidInstances;
17
17
  maxYorInstances;
18
+ maxFrankyInstances;
18
19
  logger;
19
20
  shuttingDown = false;
20
21
  constructor(config) {
21
22
  super();
22
23
  this.maxLoidInstances = config.maxLoidInstances;
23
24
  this.maxYorInstances = config.maxYorInstances;
25
+ this.maxFrankyInstances = config.maxFrankyInstances ?? 2;
24
26
  this.logger = config.logger ?? { info: console.log, error: console.error };
25
27
  }
26
28
  // ── 生命周期 ──
@@ -33,7 +35,9 @@ export class CCBroker extends EventEmitter {
33
35
  }
34
36
  // 并发检查
35
37
  const currentCount = this.countByRole(options.role);
36
- const maxCount = options.role === 'loid' ? this.maxLoidInstances : this.maxYorInstances;
38
+ const maxCount = options.role === 'loid' ? this.maxLoidInstances
39
+ : options.role === 'yor' ? this.maxYorInstances
40
+ : this.maxFrankyInstances;
37
41
  if (currentCount >= maxCount) {
38
42
  throw new Error(`${options.role} 实例已满(${currentCount}/${maxCount})`);
39
43
  }
@@ -0,0 +1,160 @@
1
+ import { readFile, readdir, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+ import { execSync } from 'node:child_process';
5
+ import { getTopic, getRecentMessages } from '@team-anya/db';
6
+ /**
7
+ * 为 Franky CC 实例重建上下文
8
+ * 按 checkpoint 截断策略:所有 checkpoint 标题 + 最近 2 个详情 + 末个 checkpoint 后对话
9
+ */
10
+ export async function buildTopicContext(deps, topicId) {
11
+ const topic = getTopic(deps.db, topicId);
12
+ if (!topic)
13
+ throw new Error(`专项 ${topicId} 不存在`);
14
+ const topicDir = join(deps.workspacePath, 'franky', 'topics', topicId);
15
+ const sections = [];
16
+ // 1. Topic 元信息
17
+ sections.push(`# 专项上下文:${topic.title} (${topic.id})`);
18
+ sections.push('');
19
+ sections.push('## 目标');
20
+ sections.push(topic.description ?? '(未设定)');
21
+ sections.push('');
22
+ sections.push('## 通讯信息');
23
+ if (topic.chat_id)
24
+ sections.push(`- 群聊 ID:\`${topic.chat_id}\``);
25
+ if (topic.root_message_id)
26
+ sections.push(`- 话题根消息 ID:\`${topic.root_message_id}\``);
27
+ if (topic.thread_id)
28
+ sections.push(`- 话题 ID:\`${topic.thread_id}\`(仅供参考,channel.send 的 reply_to 请用根消息 ID)`);
29
+ sections.push('');
30
+ sections.push('## 项目信息');
31
+ sections.push(`- 专项 ID:${topic.id}`);
32
+ sections.push(`- 状态:${topic.status}`);
33
+ if (topic.project_id)
34
+ sections.push(`- 关联项目:${topic.project_id}`);
35
+ if (topic.branch_name)
36
+ sections.push(`- 分支:${topic.branch_name}`);
37
+ sections.push('');
38
+ // 1.5 代码工作区(worktree 信息)
39
+ if (topic.project_id && topic.workspace_path) {
40
+ try {
41
+ const entries = await readdir(topic.workspace_path, { withFileTypes: true });
42
+ const worktreeDirs = entries
43
+ .filter(e => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'checkpoints')
44
+ .map(e => e.name);
45
+ if (worktreeDirs.length > 0) {
46
+ sections.push('## 代码工作区');
47
+ sections.push('');
48
+ for (const dir of worktreeDirs) {
49
+ const fullPath = join(topic.workspace_path, dir);
50
+ const branchInfo = topic.branch_name ? `分支 \`${topic.branch_name}\`,` : '';
51
+ sections.push(`- \`${dir}/\` → ${branchInfo}路径 \`${fullPath}\``);
52
+ }
53
+ sections.push('');
54
+ sections.push('> 代码在上述 worktree 子目录中,git 操作请进入对应子目录执行。');
55
+ sections.push('');
56
+ }
57
+ }
58
+ catch {
59
+ // workspace 不可读,跳过
60
+ }
61
+ }
62
+ // 2. Checkpoints
63
+ const checkpointsDir = join(topicDir, 'checkpoints');
64
+ let checkpointFiles = [];
65
+ if (existsSync(checkpointsDir)) {
66
+ const files = await readdir(checkpointsDir);
67
+ checkpointFiles = files.filter(f => f.match(/^\d{3}-.*\.md$/)).sort();
68
+ }
69
+ if (checkpointFiles.length > 0) {
70
+ sections.push('## Checkpoint 概览');
71
+ for (const file of checkpointFiles) {
72
+ const num = file.split('-')[0];
73
+ const name = file.replace(/^\d{3}-/, '').replace(/\.md$/, '');
74
+ sections.push(`${parseInt(num, 10)}. ${name}`);
75
+ }
76
+ sections.push('');
77
+ // 最近 2 个 checkpoint 的完整内容
78
+ const recentCheckpoints = checkpointFiles.slice(-2);
79
+ sections.push('## 最近 Checkpoint 详情');
80
+ sections.push('');
81
+ for (const file of recentCheckpoints) {
82
+ try {
83
+ const content = await readFile(join(checkpointsDir, file), 'utf-8');
84
+ sections.push(content.trim());
85
+ sections.push('');
86
+ }
87
+ catch {
88
+ // 文件读取失败,跳过
89
+ }
90
+ }
91
+ }
92
+ // 3. Git log(如果有 worktree)
93
+ const gitDir = join(topicDir);
94
+ try {
95
+ const gitLog = execSync('git log --oneline -10 2>/dev/null || true', {
96
+ cwd: gitDir,
97
+ encoding: 'utf-8',
98
+ timeout: 5000,
99
+ }).trim();
100
+ if (gitLog) {
101
+ sections.push('## 近期 Git 记录');
102
+ sections.push('```');
103
+ sections.push(gitLog);
104
+ sections.push('```');
105
+ sections.push('');
106
+ }
107
+ }
108
+ catch {
109
+ // 不是 git 仓库或命令失败,跳过
110
+ }
111
+ // 4. 最后一个 checkpoint 之后的对话(用 thread_id 所在的 chat_id + since 过滤)
112
+ if (topic.chat_id) {
113
+ try {
114
+ // 查找最后一个 checkpoint 的时间戳,只拉取其后的消息
115
+ let since;
116
+ if (checkpointFiles.length > 0) {
117
+ try {
118
+ const lastCheckpointPath = join(checkpointsDir, checkpointFiles[checkpointFiles.length - 1]);
119
+ const stat = await import('node:fs/promises').then(m => m.stat(lastCheckpointPath));
120
+ since = stat.mtime.toISOString().replace(/\.\d{3}Z$/, 'Z');
121
+ }
122
+ catch {
123
+ // stat 失败,不设 since 过滤
124
+ }
125
+ }
126
+ const messages = getRecentMessages(deps.db, {
127
+ chat_id: topic.chat_id,
128
+ limit: 30,
129
+ sort: 'asc',
130
+ ...(since ? { since } : {}),
131
+ });
132
+ if (messages.length > 0) {
133
+ sections.push('## 近期对话');
134
+ sections.push('');
135
+ for (const msg of messages) {
136
+ const direction = msg.direction === 'outbound' ? 'Anya' : (msg.sender_name ?? msg.sender ?? '用户');
137
+ sections.push(`**${direction}** (${msg.created_at}):`);
138
+ // 截断过长的消息
139
+ const content = msg.content.length > 500 ? msg.content.slice(0, 500) + '...' : msg.content;
140
+ sections.push(content);
141
+ sections.push('');
142
+ }
143
+ }
144
+ }
145
+ catch (err) {
146
+ deps.logger?.error('[TopicContextBuilder] 查询对话历史失败:', err);
147
+ }
148
+ }
149
+ return sections.join('\n');
150
+ }
151
+ /**
152
+ * 生成并写入 context.md 文件
153
+ */
154
+ export async function writeTopicContext(deps, topicId) {
155
+ const content = await buildTopicContext(deps, topicId);
156
+ const contextPath = join(deps.workspacePath, 'franky', 'topics', topicId, 'context.md');
157
+ await writeFile(contextPath, content, 'utf-8');
158
+ return contextPath;
159
+ }
160
+ //# sourceMappingURL=context-builder.js.map
@@ -0,0 +1,107 @@
1
+ import { createServer } from 'node:http';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { Server } from '@modelcontextprotocol/sdk/server';
4
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
5
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
6
+ import { zodToJsonSchema } from 'zod-to-json-schema';
7
+ import { createToolRouter } from '@team-anya/mcp-tools';
8
+ // ── MCP Server 创建(薄路由层) ──
9
+ function createFrankyMcpServer(deps) {
10
+ const logger = deps.logger ?? { info: console.log, error: console.error };
11
+ const routerDeps = {
12
+ db: deps.db,
13
+ workspacePath: deps.workspacePath,
14
+ currentTopicId: deps.topicId,
15
+ replyToThreadId: deps.replyToThreadId,
16
+ channelRegistry: deps.channelRegistry,
17
+ onMessageSent: deps.onMessageSent,
18
+ onTopicClosed: deps.onTopicClosed,
19
+ onTopicEscalated: deps.onTopicEscalated,
20
+ logger,
21
+ };
22
+ const router = createToolRouter('franky', routerDeps);
23
+ const server = new Server({ name: 'franky-mcp', version: '1.0.0' }, { capabilities: { tools: {} } });
24
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
25
+ const tools = router.tools.map(def => ({
26
+ name: def.name,
27
+ description: def.description,
28
+ inputSchema: zodToJsonSchema(def.inputSchema),
29
+ }));
30
+ return { tools };
31
+ });
32
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
33
+ const { name, arguments: args } = request.params;
34
+ return router.handleToolCall(name, args ?? {});
35
+ });
36
+ return server;
37
+ }
38
+ /**
39
+ * 启动 Franky MCP server(薄路由层)
40
+ *
41
+ * Franky 可以使用 Layer 1 全部共享工具 + Layer 3 Franky 专属工具(channel.send 等)。
42
+ * 不需要 Yor 的 deliver/block 工具。
43
+ *
44
+ * 所有工具实现来自 @team-anya/mcp-tools。
45
+ */
46
+ export async function startFrankyMcpServer(deps) {
47
+ const sessions = new Map();
48
+ const httpServer = createServer(async (req, res) => {
49
+ const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
50
+ if (url.pathname !== '/mcp') {
51
+ res.writeHead(404);
52
+ res.end('Not Found');
53
+ return;
54
+ }
55
+ const sessionId = req.headers['mcp-session-id'];
56
+ if (sessionId && sessions.has(sessionId)) {
57
+ const { transport } = sessions.get(sessionId);
58
+ await transport.handleRequest(req, res);
59
+ return;
60
+ }
61
+ if (req.method === 'POST') {
62
+ const transport = new StreamableHTTPServerTransport({
63
+ sessionIdGenerator: () => randomUUID(),
64
+ });
65
+ const server = createFrankyMcpServer(deps);
66
+ transport.onclose = () => {
67
+ if (transport.sessionId) {
68
+ sessions.delete(transport.sessionId);
69
+ }
70
+ };
71
+ await server.connect(transport);
72
+ await transport.handleRequest(req, res);
73
+ if (transport.sessionId) {
74
+ sessions.set(transport.sessionId, { server, transport });
75
+ }
76
+ return;
77
+ }
78
+ res.writeHead(400);
79
+ res.end('Bad Request: No valid session');
80
+ });
81
+ await new Promise((resolve) => {
82
+ httpServer.listen(0, '127.0.0.1', resolve);
83
+ });
84
+ const addr = httpServer.address();
85
+ const port = typeof addr === 'object' && addr !== null ? addr.port : 0;
86
+ const serverUrl = `http://127.0.0.1:${port}/mcp`;
87
+ return {
88
+ port,
89
+ url: serverUrl,
90
+ close: async () => {
91
+ for (const { server, transport } of sessions.values()) {
92
+ await transport.close();
93
+ await server.close();
94
+ }
95
+ sessions.clear();
96
+ await new Promise((resolve, reject) => {
97
+ httpServer.close((err) => {
98
+ if (err)
99
+ reject(err);
100
+ else
101
+ resolve();
102
+ });
103
+ });
104
+ },
105
+ };
106
+ }
107
+ //# sourceMappingURL=franky-mcp-server.js.map