team-anya 0.2.0

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 (145) hide show
  1. package/README.md +38 -0
  2. package/apps/server/dist/broker/cc-broker.js +267 -0
  3. package/apps/server/dist/cli.js +296 -0
  4. package/apps/server/dist/config.js +78 -0
  5. package/apps/server/dist/daemon.js +51 -0
  6. package/apps/server/dist/franky/context-builder.js +161 -0
  7. package/apps/server/dist/franky/franky-mcp-server.js +110 -0
  8. package/apps/server/dist/franky/franky-orchestrator.js +629 -0
  9. package/apps/server/dist/franky/index.js +5 -0
  10. package/apps/server/dist/franky/topic-router.js +16 -0
  11. package/apps/server/dist/gateway/chat-sync.js +135 -0
  12. package/apps/server/dist/gateway/command-router.js +116 -0
  13. package/apps/server/dist/gateway/commands/cancel.js +32 -0
  14. package/apps/server/dist/gateway/commands/help.js +16 -0
  15. package/apps/server/dist/gateway/commands/index.js +26 -0
  16. package/apps/server/dist/gateway/commands/restart.js +43 -0
  17. package/apps/server/dist/gateway/commands/status.js +34 -0
  18. package/apps/server/dist/gateway/commands/tasks.js +33 -0
  19. package/apps/server/dist/gateway/feishu-sender.js +508 -0
  20. package/apps/server/dist/gateway/feishu-ws.js +353 -0
  21. package/apps/server/dist/gateway/health-monitor.js +154 -0
  22. package/apps/server/dist/gateway/http.js +1064 -0
  23. package/apps/server/dist/gateway/media-downloader.js +182 -0
  24. package/apps/server/dist/gateway/message-events.js +10 -0
  25. package/apps/server/dist/gateway/message-intake.js +72 -0
  26. package/apps/server/dist/gateway/message-queue.js +118 -0
  27. package/apps/server/dist/gateway/session-reader.js +142 -0
  28. package/apps/server/dist/gateway/ws-push.js +115 -0
  29. package/apps/server/dist/loid/brain.js +121 -0
  30. package/apps/server/dist/loid/clarifier.js +162 -0
  31. package/apps/server/dist/loid/context-builder.js +462 -0
  32. package/apps/server/dist/loid/mcp-server.js +119 -0
  33. package/apps/server/dist/loid/memory-settler.js +189 -0
  34. package/apps/server/dist/loid/opportunity-manager.js +148 -0
  35. package/apps/server/dist/loid/profile-updater.js +179 -0
  36. package/apps/server/dist/loid/project-registry.js +192 -0
  37. package/apps/server/dist/loid/reporter.js +148 -0
  38. package/apps/server/dist/loid/schemas.js +117 -0
  39. package/apps/server/dist/loid/self-calibrator.js +314 -0
  40. package/apps/server/dist/loid/session-manager.js +472 -0
  41. package/apps/server/dist/loid/session.js +276 -0
  42. package/apps/server/dist/main.js +528 -0
  43. package/apps/server/dist/tracing/index.js +2 -0
  44. package/apps/server/dist/tracing/trace-context.js +92 -0
  45. package/apps/server/dist/types/message.js +2 -0
  46. package/apps/server/dist/yor/yor-mcp-server.js +107 -0
  47. package/apps/server/dist/yor/yor-orchestrator.js +248 -0
  48. package/apps/web/dist/assets/index-BiiEB0qZ.css +1 -0
  49. package/apps/web/dist/assets/index-Dnb9LGZd.js +798 -0
  50. package/apps/web/dist/index.html +13 -0
  51. package/package.json +42 -0
  52. package/packages/cc-client/dist/claude-code-backend.js +792 -0
  53. package/packages/cc-client/dist/index.js +2 -0
  54. package/packages/cc-client/package.json +11 -0
  55. package/packages/core/dist/constants.js +60 -0
  56. package/packages/core/dist/errors.js +35 -0
  57. package/packages/core/dist/index.js +9 -0
  58. package/packages/core/dist/office-init.js +190 -0
  59. package/packages/core/dist/repo-cache.js +70 -0
  60. package/packages/core/dist/scope/checker.js +114 -0
  61. package/packages/core/dist/scope/defaults.js +55 -0
  62. package/packages/core/dist/scope/index.js +3 -0
  63. package/packages/core/dist/state-machine.js +86 -0
  64. package/packages/core/dist/types/audit.js +12 -0
  65. package/packages/core/dist/types/backend.js +2 -0
  66. package/packages/core/dist/types/commitment.js +17 -0
  67. package/packages/core/dist/types/communication.js +18 -0
  68. package/packages/core/dist/types/index.js +9 -0
  69. package/packages/core/dist/types/opportunity.js +27 -0
  70. package/packages/core/dist/types/org.js +26 -0
  71. package/packages/core/dist/types/task.js +46 -0
  72. package/packages/core/dist/types/workspace.js +39 -0
  73. package/packages/core/dist/workspace-manager.js +314 -0
  74. package/packages/core/package.json +10 -0
  75. package/packages/db/dist/client.js +69 -0
  76. package/packages/db/dist/index.js +756 -0
  77. package/packages/db/dist/schema/audit-events.js +13 -0
  78. package/packages/db/dist/schema/cc-sessions.js +14 -0
  79. package/packages/db/dist/schema/chats.js +35 -0
  80. package/packages/db/dist/schema/commitments.js +18 -0
  81. package/packages/db/dist/schema/communication-events.js +14 -0
  82. package/packages/db/dist/schema/index.js +14 -0
  83. package/packages/db/dist/schema/message-log.js +20 -0
  84. package/packages/db/dist/schema/opportunities.js +23 -0
  85. package/packages/db/dist/schema/org.js +36 -0
  86. package/packages/db/dist/schema/projects.js +23 -0
  87. package/packages/db/dist/schema/tasks.js +51 -0
  88. package/packages/db/dist/schema/topics.js +22 -0
  89. package/packages/db/dist/schema/trace-spans.js +19 -0
  90. package/packages/db/dist/schema/workspaces.js +15 -0
  91. package/packages/db/package.json +12 -0
  92. package/packages/db/src/migrations/0000_baseline.sql +251 -0
  93. package/packages/db/src/migrations/0001_workspaces.sql +19 -0
  94. package/packages/db/src/migrations/0002_workspace_parent.sql +1 -0
  95. package/packages/db/src/migrations/0003_chat_context.sql +3 -0
  96. package/packages/db/src/migrations/meta/_journal.json +34 -0
  97. package/packages/mcp-tools/dist/index.js +41 -0
  98. package/packages/mcp-tools/dist/layer1/audit-append.js +38 -0
  99. package/packages/mcp-tools/dist/layer1/audit-query.js +51 -0
  100. package/packages/mcp-tools/dist/layer1/memory-brief.js +168 -0
  101. package/packages/mcp-tools/dist/layer1/memory-context.js +124 -0
  102. package/packages/mcp-tools/dist/layer1/memory-digest.js +126 -0
  103. package/packages/mcp-tools/dist/layer1/memory-forget.js +108 -0
  104. package/packages/mcp-tools/dist/layer1/memory-learn.js +63 -0
  105. package/packages/mcp-tools/dist/layer1/memory-recall.js +287 -0
  106. package/packages/mcp-tools/dist/layer1/memory-reflect.js +80 -0
  107. package/packages/mcp-tools/dist/layer1/memory-remember.js +119 -0
  108. package/packages/mcp-tools/dist/layer1/memory-search.js +263 -0
  109. package/packages/mcp-tools/dist/layer1/memory-write.js +21 -0
  110. package/packages/mcp-tools/dist/layer1/org-lookup.js +47 -0
  111. package/packages/mcp-tools/dist/layer1/project-get.js +28 -0
  112. package/packages/mcp-tools/dist/layer1/project-list.js +20 -0
  113. package/packages/mcp-tools/dist/layer1/report-daily.js +68 -0
  114. package/packages/mcp-tools/dist/layer1/task-get.js +29 -0
  115. package/packages/mcp-tools/dist/layer1/task-update.js +34 -0
  116. package/packages/mcp-tools/dist/layer2/franky/topic-checkpoint.js +43 -0
  117. package/packages/mcp-tools/dist/layer2/franky/topic-escalate.js +19 -0
  118. package/packages/mcp-tools/dist/layer2/loid/decision-log.js +15 -0
  119. package/packages/mcp-tools/dist/layer2/loid/decision-no-action.js +15 -0
  120. package/packages/mcp-tools/dist/layer2/loid/delivery-create-pr.js +30 -0
  121. package/packages/mcp-tools/dist/layer2/loid/delivery-share.js +12 -0
  122. package/packages/mcp-tools/dist/layer2/loid/delivery-submit.js +77 -0
  123. package/packages/mcp-tools/dist/layer2/loid/delivery-upload.js +18 -0
  124. package/packages/mcp-tools/dist/layer2/loid/project-remove.js +16 -0
  125. package/packages/mcp-tools/dist/layer2/loid/project-upsert.js +33 -0
  126. package/packages/mcp-tools/dist/layer2/loid/task-dispatch.js +206 -0
  127. package/packages/mcp-tools/dist/layer2/loid/task-escalate-to-topic.js +170 -0
  128. package/packages/mcp-tools/dist/layer2/loid/task-lookup.js +45 -0
  129. package/packages/mcp-tools/dist/layer2/loid/topic-close.js +22 -0
  130. package/packages/mcp-tools/dist/layer2/loid/topic-create.js +60 -0
  131. package/packages/mcp-tools/dist/layer2/loid/yor-approve.js +8 -0
  132. package/packages/mcp-tools/dist/layer2/loid/yor-kill.js +7 -0
  133. package/packages/mcp-tools/dist/layer2/loid/yor-rework.js +7 -0
  134. package/packages/mcp-tools/dist/layer2/loid/yor-spawn.js +28 -0
  135. package/packages/mcp-tools/dist/layer2/loid/yor-status.js +8 -0
  136. package/packages/mcp-tools/dist/layer2/yor/task-block.js +11 -0
  137. package/packages/mcp-tools/dist/layer2/yor/task-deliver.js +35 -0
  138. package/packages/mcp-tools/dist/layer2/yor/task-progress.js +21 -0
  139. package/packages/mcp-tools/dist/layer3/adapters/feishu-adapter.js +203 -0
  140. package/packages/mcp-tools/dist/layer3/adapters/types.js +28 -0
  141. package/packages/mcp-tools/dist/layer3/channel-receive.js +11 -0
  142. package/packages/mcp-tools/dist/layer3/channel-send.js +75 -0
  143. package/packages/mcp-tools/dist/layer3/file-upload.js +44 -0
  144. package/packages/mcp-tools/dist/registry.js +911 -0
  145. package/packages/mcp-tools/package.json +13 -0
@@ -0,0 +1,528 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { existsSync, mkdirSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import Fastify from 'fastify';
5
+ import fastifyCors from '@fastify/cors';
6
+ import fastifyStatic from '@fastify/static';
7
+ import { createDB, getTask as dbGetTask, getTopic as dbGetTopic } from '@team-anya/db';
8
+ import { memoryRemember, auditAppend, ChannelRegistry, FeishuAdapter } from '@team-anya/mcp-tools';
9
+ import { ensureOffice, ensureWorkspacesDir, TaskStatus, RepoCache, WorkspaceManager } from '@team-anya/core';
10
+ import { createWorkspaceRecord, getWorkspaceRecord, updateWorkspaceRecord, getWorkspaceByOwner } from '@team-anya/db';
11
+ import { loadConfig } from './config.js';
12
+ import { registerRoutes } from './gateway/http.js';
13
+ import { CCBroker } from './broker/cc-broker.js';
14
+ import { YorOrchestrator } from './yor/yor-orchestrator.js';
15
+ import { FeishuWSClient } from './gateway/feishu-ws.js';
16
+ import { MediaDownloader } from './gateway/media-downloader.js';
17
+ import { ChatSyncService } from './gateway/chat-sync.js';
18
+ import { FeishuSender } from './gateway/feishu-sender.js';
19
+ import { MessageIntake } from './gateway/message-intake.js';
20
+ import { CommandRouter } from './gateway/command-router.js';
21
+ import { registerBuiltinCommands } from './gateway/commands/index.js';
22
+ import { MessageQueue } from './gateway/message-queue.js';
23
+ import { MemorySettler } from './loid/memory-settler.js';
24
+ import { LoidBrain } from './loid/brain.js';
25
+ import { LoidContextBuilder } from './loid/context-builder.js';
26
+ import { Clarifier } from './loid/clarifier.js';
27
+ import { ProjectRegistry } from './loid/project-registry.js';
28
+ import { FrankyOrchestrator } from './franky/franky-orchestrator.js';
29
+ import { FrankyTopicRouter } from './franky/topic-router.js';
30
+ import { HealthMonitor } from './gateway/health-monitor.js';
31
+ export async function buildServer() {
32
+ const config = loadConfig();
33
+ // 初始化 office 目录(从模板源复制缺失的文件)
34
+ await ensureOffice(config.WORKSPACE_PATH, resolve(config.OFFICE_TEMPLATE_DIR), { info: (msg) => console.log(`[office-init] ${msg}`) });
35
+ const db = createDB(config.SQLITE_PATH);
36
+ // 日志配置:stdout + 文件双写(按日期+大小分割)
37
+ let loggerConfig = false;
38
+ if (config.NODE_ENV !== 'test') {
39
+ const logDir = resolve(config.LOG_DIR);
40
+ mkdirSync(logDir, { recursive: true });
41
+ loggerConfig = {
42
+ level: 'info',
43
+ transport: {
44
+ targets: [
45
+ { target: 'pino-pretty', options: { destination: 1 }, level: 'info' },
46
+ {
47
+ target: 'pino-roll',
48
+ options: {
49
+ file: join(logDir, 'anya'),
50
+ frequency: 'daily',
51
+ limit: { count: 14 },
52
+ mkdir: true,
53
+ },
54
+ level: 'info',
55
+ },
56
+ ],
57
+ },
58
+ };
59
+ }
60
+ const app = Fastify({
61
+ logger: loggerConfig,
62
+ disableRequestLogging: true,
63
+ });
64
+ // CORS
65
+ if (config.NODE_ENV === 'development') {
66
+ await app.register(fastifyCors, { origin: 'http://localhost:5173' });
67
+ }
68
+ // 静态文件(兼容 monorepo 开发 + npm 全局安装两种目录结构)
69
+ const webDistCandidates = [
70
+ resolve(import.meta.dirname, '../../web/dist'), // npm 包: apps/server/dist/ → apps/web/dist/
71
+ resolve(import.meta.dirname, '../../../apps/web/dist'), // monorepo 开发: src/ 下通过 tsx 运行
72
+ ];
73
+ const webDistPath = webDistCandidates.find(p => existsSync(p)) ?? webDistCandidates[0];
74
+ const serveStatic = config.NODE_ENV !== 'test' && existsSync(webDistPath);
75
+ if (serveStatic) {
76
+ await app.register(fastifyStatic, {
77
+ root: webDistPath,
78
+ prefix: '/',
79
+ });
80
+ }
81
+ // FeishuSender + 共享 Lark Client
82
+ let larkClient;
83
+ const feishuSender = (config.FEISHU_APP_ID && config.FEISHU_APP_SECRET)
84
+ ? new FeishuSender({ appId: config.FEISHU_APP_ID, appSecret: config.FEISHU_APP_SECRET, db })
85
+ : null;
86
+ if (config.FEISHU_APP_ID && config.FEISHU_APP_SECRET) {
87
+ const Lark = await import('@larksuiteoapi/node-sdk');
88
+ larkClient = new Lark.Client({
89
+ appId: config.FEISHU_APP_ID,
90
+ appSecret: config.FEISHU_APP_SECRET,
91
+ });
92
+ }
93
+ // ChatSyncService(飞书群信息同步)
94
+ const chatSyncService = larkClient ? new ChatSyncService(larkClient, db) : null;
95
+ // ChannelRegistry(通道注册表)
96
+ const channelRegistry = new ChannelRegistry();
97
+ if (feishuSender) {
98
+ channelRegistry.register(new FeishuAdapter(feishuSender));
99
+ app.log.info('已注册飞书通道适配器');
100
+ }
101
+ // MemorySettler
102
+ const memorySettler = new MemorySettler({
103
+ readFile: async (path) => {
104
+ try {
105
+ return await readFile(path, 'utf-8');
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ },
111
+ memoryRemember: (input) => memoryRemember(db, config.WORKSPACE_PATH, input),
112
+ auditAppend: (input) => auditAppend(db, config.WORKSPACE_PATH, input).then(() => { }),
113
+ });
114
+ // 任务完成后处理回调
115
+ const onTaskComplete = async (taskId, exitCode) => {
116
+ if (exitCode !== 0)
117
+ return;
118
+ try {
119
+ const taskDir = join(config.WORKSPACE_PATH, 'yor', 'tasks', taskId);
120
+ await memorySettler.settle(taskId, taskDir);
121
+ }
122
+ catch (err) {
123
+ console.error('经验沉淀失败:', err);
124
+ }
125
+ };
126
+ // Clarifier(保留用于 HTTP API)
127
+ const clarifier = new Clarifier({ db });
128
+ // ProjectRegistry — 项目注册表
129
+ const projectRegistry = new ProjectRegistry({
130
+ workspacePath: resolve(config.WORKSPACE_PATH),
131
+ projectsConfigPath: resolve(config.PROJECTS_CONFIG_PATH),
132
+ reposPath: resolve(config.REPOS_PATH),
133
+ db,
134
+ });
135
+ // 自动初始化仓库(首次使用时 clone 缺失的仓库)
136
+ const ensureResult = await projectRegistry.ensureRepos();
137
+ if (ensureResult.cloned.length > 0) {
138
+ app.log.info(`自动 clone 仓库: ${ensureResult.cloned.join(', ')}`);
139
+ }
140
+ if (ensureResult.failed.length > 0) {
141
+ for (const f of ensureResult.failed) {
142
+ app.log.error(`仓库初始化失败 (${f.name}): ${f.error}`);
143
+ }
144
+ }
145
+ // WorkspaceManager(clone-based 工作区管理)
146
+ await ensureWorkspacesDir(config.ANYA_HOME, resolve(config.OFFICE_TEMPLATE_DIR), { info: (msg) => console.log(`[office-init] ${msg}`) });
147
+ const repoCache = new RepoCache({
148
+ reposPath: resolve(config.REPOS_PATH),
149
+ logger: { info: (msg) => app.log.info(msg) },
150
+ });
151
+ const workspaceStore = {
152
+ create: (data) => createWorkspaceRecord(db, data),
153
+ get: (id) => getWorkspaceRecord(db, id),
154
+ update: (id, data) => updateWorkspaceRecord(db, id, data),
155
+ findByOwner: (ownerId) => getWorkspaceByOwner(db, ownerId),
156
+ };
157
+ const workspaceManager = new WorkspaceManager(workspaceStore, {
158
+ workspacesPath: resolve(config.WORKSPACES_PATH),
159
+ templateDir: resolve(config.OFFICE_TEMPLATE_DIR),
160
+ logger: { info: (msg) => app.log.info(msg) },
161
+ }, repoCache);
162
+ // ContextBuilder
163
+ const contextBuilder = new LoidContextBuilder({
164
+ db,
165
+ workspacePath: config.WORKSPACE_PATH,
166
+ projectRegistry,
167
+ });
168
+ // CCBroker — 统一 CC 进程管理器
169
+ const broker = new CCBroker({
170
+ maxLoidInstances: 2,
171
+ maxYorInstances: config.MAX_YOR_CONCURRENCY,
172
+ maxFrankyInstances: 2,
173
+ logger: app.log,
174
+ });
175
+ // YorOrchestrator — 替代 Yor 子进程,通过 CCBroker 管理 Yor CC
176
+ const yorOrchestrator = new YorOrchestrator(broker, {
177
+ binary: config.CLAUDE_CODE_BINARY,
178
+ workspacePath: config.WORKSPACE_PATH,
179
+ db,
180
+ logger: app.log,
181
+ });
182
+ // ── Franky Orchestrator ──
183
+ const frankyOrchestrator = new FrankyOrchestrator(broker, {
184
+ binary: config.CLAUDE_CODE_BINARY,
185
+ workspacePath: config.WORKSPACE_PATH,
186
+ db,
187
+ channelRegistry,
188
+ workspaceManager,
189
+ onMessageSent: feishuSender
190
+ ? (chatId) => { feishuSender.clearTypingReaction(chatId).catch(() => { }); }
191
+ : undefined,
192
+ onTopicEscalated: (topicId, reason, category) => {
193
+ // 通过飞书通知让相关人感知升级事件(审计日志已由 topic.escalate 工具写入)
194
+ if (feishuSender) {
195
+ const topic = dbGetTopic(db, topicId);
196
+ const chatId = topic?.chat_id;
197
+ if (chatId) {
198
+ feishuSender.sendText({
199
+ receiveIdType: 'chat_id',
200
+ receiveId: chatId,
201
+ text: `[Franky 升级] 话题「${topic?.title ?? topicId}」需要介入(${category}):${reason}`,
202
+ }).catch(err => {
203
+ app.log.error({ err, topicId }, 'Franky 升级飞书通知发送失败');
204
+ });
205
+ }
206
+ }
207
+ app.log.info({ topicId, category, reason }, 'Franky 升级请求');
208
+ },
209
+ onRateLimited: (topicId, topicTitle, resetsAt) => {
210
+ app.log.info({ topicId, resetsAt }, 'Franky 实例限流');
211
+ if (feishuSender) {
212
+ const topic = dbGetTopic(db, topicId);
213
+ const chatId = topic?.chat_id;
214
+ if (chatId) {
215
+ const resetTime = new Date(resetsAt * 1000).toLocaleTimeString('zh-CN', {
216
+ hour: '2-digit', minute: '2-digit', timeZone: 'Asia/Shanghai',
217
+ });
218
+ feishuSender.sendText({
219
+ receiveIdType: 'chat_id',
220
+ receiveId: chatId,
221
+ text: `🙋 Franky 请假通知\nFranky 在「${topicTitle}」话题中聊得太投入,需要喘口气。预计 ${resetTime} 回来继续。`,
222
+ }).catch(err => {
223
+ app.log.error({ err, topicId }, 'Franky 限流通知发送失败');
224
+ });
225
+ }
226
+ }
227
+ },
228
+ onCrash: (topicId, topicTitle) => {
229
+ app.log.error({ topicId }, 'Franky 实例崩溃');
230
+ if (feishuSender) {
231
+ const topic = dbGetTopic(db, topicId);
232
+ const chatId = topic?.chat_id;
233
+ if (chatId) {
234
+ const replies = [
235
+ '不好意思,刚才掉线了,你再说一下我接上',
236
+ '抱歉断了一下,你可以继续说,我会重新接上的',
237
+ '出了点小状况,不过没事,你继续说就行',
238
+ ];
239
+ feishuSender.sendText({
240
+ receiveIdType: 'chat_id',
241
+ receiveId: chatId,
242
+ text: replies[Math.floor(Math.random() * replies.length)],
243
+ }).catch(err => {
244
+ app.log.error({ err, topicId }, 'Franky 崩溃通知发送失败');
245
+ });
246
+ }
247
+ }
248
+ },
249
+ getProjectConfig: (projectId) => projectRegistry.getProjectConfig(projectId),
250
+ logger: app.log,
251
+ });
252
+ const topicRouter = new FrankyTopicRouter(frankyOrchestrator, app.log);
253
+ // LoidBrain(指挥模式)
254
+ const loidBrain = new LoidBrain({
255
+ binary: config.CLAUDE_CODE_BINARY,
256
+ loidBaseDir: resolve(config.LOID_BASE_DIR),
257
+ officePath: resolve(config.WORKSPACE_PATH),
258
+ templateDir: resolve(config.OFFICE_TEMPLATE_DIR),
259
+ protocolsDir: resolve(config.LOID_PROTOCOLS_DIR),
260
+ maxTurnsPerSession: 50,
261
+ logger: app.log,
262
+ }, {
263
+ broker,
264
+ feishuSender: feishuSender ?? undefined,
265
+ projectRegistry,
266
+ mcpDeps: {
267
+ db,
268
+ channelRegistry,
269
+ workspacePath: config.WORKSPACE_PATH,
270
+ logger: app.log,
271
+ yorOrchestrator,
272
+ threadCreator: feishuSender ?? undefined,
273
+ getProjectConfig: (projectId) => projectRegistry.getProjectConfig(projectId),
274
+ onProjectChanged: () => projectRegistry.invalidateCache(),
275
+ workspaceManager,
276
+ onMessageSent: feishuSender
277
+ ? (chatId) => { feishuSender.clearTypingReaction(chatId).catch(() => { }); }
278
+ : undefined,
279
+ onTaskEscalated: (topicId, workspacePath, followUpMessage) => {
280
+ app.log.info({ topicId, workspacePath, hasFollowUp: !!followUpMessage }, '任务已升级为话题');
281
+ if (followUpMessage) {
282
+ // 立即启动 Franky 并转发用户的续做请求
283
+ frankyOrchestrator.sendMessage(topicId, {
284
+ content: followUpMessage,
285
+ media: [],
286
+ isDirectMessage: false,
287
+ mentionsAnya: true,
288
+ }).catch(err => {
289
+ app.log.error({ topicId, err: err instanceof Error ? err.message : String(err) }, '启动 Franky 处理续做请求失败');
290
+ });
291
+ }
292
+ },
293
+ },
294
+ });
295
+ await loidBrain.init();
296
+ app.log.info('LoidBrain (指挥模式) 已启动');
297
+ // YorOrchestrator 回调 → 状态变更 + 通知 LoidBrain
298
+ yorOrchestrator.setCallbacks({
299
+ onDelivery: (taskId, _data) => {
300
+ // IN_PROGRESS → DELIVERING
301
+ yorOrchestrator.transitionTask(taskId, TaskStatus.DELIVERING, 'yor', 'Yor 提交交付');
302
+ const ctx = contextBuilder.buildDeliveryContext(taskId, 0);
303
+ loidBrain.handleDelivery(ctx).catch(err => {
304
+ app.log.error({ err, taskId }, 'Yor 交付验收触发失败');
305
+ });
306
+ },
307
+ onBlocker: (taskId, data) => {
308
+ // IN_PROGRESS → BLOCKED
309
+ yorOrchestrator.transitionTask(taskId, TaskStatus.BLOCKED, 'yor', `阻塞: ${data.category} - ${data.reason}`);
310
+ app.log.info({ taskId, category: data.category }, 'Yor 报告阻塞,已更新状态');
311
+ },
312
+ onInstanceExited: (taskId) => {
313
+ onTaskComplete(taskId, 0).catch(err => {
314
+ app.log.error({ err, taskId }, '经验沉淀失败');
315
+ });
316
+ },
317
+ onRateLimited: (taskId, resetsAt, _rateLimitType) => {
318
+ app.log.info({ taskId, resetsAt }, 'Yor 实例限流');
319
+ if (feishuSender) {
320
+ const task = dbGetTask(db, taskId);
321
+ const chatId = task?.source_chat_id;
322
+ if (chatId) {
323
+ const title = task?.title ? `「${task.title}」` : taskId;
324
+ const resetTime = new Date(resetsAt * 1000).toLocaleTimeString('zh-CN', {
325
+ hour: '2-digit', minute: '2-digit', timeZone: 'Asia/Shanghai',
326
+ });
327
+ feishuSender.sendText({
328
+ receiveIdType: 'chat_id',
329
+ receiveId: chatId,
330
+ text: `🙋 Yor 请假通知\n${title} 执行过程中体力不支了,需要休息一下。预计 ${resetTime} 回来继续干活,任务已自动挂起,不用担心。`,
331
+ }).catch(err => {
332
+ app.log.error({ err, taskId }, 'Yor 限流通知发送失败');
333
+ });
334
+ }
335
+ }
336
+ },
337
+ onInstanceCrash: (taskId, error) => {
338
+ app.log.error({ taskId, error }, 'Yor 实例崩溃');
339
+ // 崩溃 → BLOCKED(非终态才变更)
340
+ yorOrchestrator.transitionTask(taskId, TaskStatus.BLOCKED, 'system', `Yor 实例崩溃: ${error}`);
341
+ // 通知用户:任务执行遇到问题
342
+ if (feishuSender) {
343
+ const task = dbGetTask(db, taskId);
344
+ const chatId = task?.source_chat_id;
345
+ if (chatId) {
346
+ const title = task?.title ? `「${task.title}」` : taskId;
347
+ feishuSender.sendText({
348
+ receiveIdType: 'chat_id',
349
+ receiveId: chatId,
350
+ text: `${title} 执行过程中出了点问题,我先看看怎么回事`,
351
+ }).catch(err => {
352
+ app.log.error({ err, taskId }, 'Yor 崩溃通知发送失败');
353
+ });
354
+ }
355
+ }
356
+ const ctx = contextBuilder.buildDeliveryContext(taskId, 1);
357
+ loidBrain.handleDelivery(ctx).catch(err => {
358
+ app.log.error({ err, taskId }, 'Yor 崩溃后验收触发失败');
359
+ });
360
+ },
361
+ });
362
+ // 重启回调:释放 Loid 会话 + Broker 全部实例
363
+ const onRestart = async () => {
364
+ // 先让 SessionManager 清理 session map(会同时 dispose broker 中的 Loid 实例)
365
+ if (loidBrain) {
366
+ await loidBrain.disposeSessions();
367
+ }
368
+ // 再释放剩余实例(Yor 等)
369
+ const ids = await broker.disposeAll();
370
+ app.log.info(`[restart] 已释放 ${ids.length} 个 CC 实例`);
371
+ return { disposed: ids.length };
372
+ };
373
+ // CommandRouter(斜杠命令拦截)
374
+ let commandRouter;
375
+ if (feishuSender) {
376
+ commandRouter = new CommandRouter({ db, feishuSender, broker, onRestart, frankyOrchestrator, loidSessionManager: loidBrain, logger: app.log });
377
+ registerBuiltinCommands(commandRouter);
378
+ app.log.info('CommandRouter 已注册内置命令');
379
+ }
380
+ // MessageIntake(事件转发器)
381
+ const messageIntake = new MessageIntake({
382
+ db,
383
+ loidBrain,
384
+ contextBuilder,
385
+ commandRouter,
386
+ topicRouter,
387
+ });
388
+ // MessageQueue
389
+ const messageQueue = new MessageQueue({
390
+ handler: async (msg) => {
391
+ try {
392
+ await messageIntake.handle(msg);
393
+ }
394
+ catch (err) {
395
+ app.log.error({ err, msg: msg.content.slice(0, 50) }, '消息处理失败');
396
+ }
397
+ },
398
+ logger: {
399
+ info: (...args) => app.log.info(args.join(' ')),
400
+ warn: (...args) => app.log.warn(args.join(' ')),
401
+ error: (...args) => app.log.error(args.join(' ')),
402
+ },
403
+ });
404
+ // HTTP 路由
405
+ await registerRoutes(app, { db, broker, clarifier, messageQueue, larkClient, chatSyncService, onRestart });
406
+ // SPA fallback
407
+ if (serveStatic) {
408
+ app.setNotFoundHandler(async (request, reply) => {
409
+ if (request.url.startsWith('/api/')) {
410
+ return reply.status(404).send({ error: 'Not Found' });
411
+ }
412
+ return reply.sendFile('index.html', webDistPath);
413
+ });
414
+ }
415
+ // ── HealthMonitor(静默健康监控)──
416
+ let healthMonitor = null;
417
+ if (feishuSender) {
418
+ healthMonitor = new HealthMonitor({
419
+ broker,
420
+ db,
421
+ feishuSender,
422
+ resolveChatId: (instanceId, role) => {
423
+ if (role === 'loid') {
424
+ // instanceId 格式: loid:${chatId} 或 loid:delivery:${taskId}
425
+ const key = instanceId.replace(/^loid:/, '');
426
+ if (key.startsWith('delivery:'))
427
+ return null; // 内部 session,不通知
428
+ return key;
429
+ }
430
+ if (role === 'yor') {
431
+ const instance = broker.getInstance(instanceId);
432
+ if (instance?.taskId) {
433
+ const task = dbGetTask(db, instance.taskId);
434
+ return task?.source_chat_id ?? null;
435
+ }
436
+ return null;
437
+ }
438
+ if (role === 'franky') {
439
+ // instanceId 格式: franky:${topicId}
440
+ const topicId = instanceId.replace(/^franky:/, '');
441
+ const topic = dbGetTopic(db, topicId);
442
+ return topic?.chat_id ?? null;
443
+ }
444
+ return null;
445
+ },
446
+ logger: app.log,
447
+ });
448
+ healthMonitor.start();
449
+ app.log.info('HealthMonitor 已启动');
450
+ }
451
+ return { app, db, config, broker, yorOrchestrator, frankyOrchestrator, messageIntake, messageQueue, feishuSender, loidBrain, larkClient, chatSyncService, healthMonitor };
452
+ }
453
+ /**
454
+ * 完整启动流程:buildServer + listen + 飞书 WS + graceful shutdown
455
+ * CLI 和直接运行都调用这个函数
456
+ */
457
+ export async function startServer() {
458
+ const serverCtx = await buildServer();
459
+ const { app, config, broker, messageQueue, feishuSender, loidBrain, larkClient, chatSyncService, db, frankyOrchestrator, healthMonitor } = serverCtx;
460
+ await app.listen({ port: config.PORT, host: '0.0.0.0' });
461
+ app.log.info(`Anya server 已启动,端口 ${config.PORT}`);
462
+ // 飞书 WebSocket 长连接
463
+ let feishuWs = null;
464
+ if (config.FEISHU_APP_ID && config.FEISHU_APP_SECRET && larkClient) {
465
+ const mediaDownloader = new MediaDownloader(larkClient, {
466
+ mediaDir: config.MEDIA_PATH,
467
+ });
468
+ await mediaDownloader.ensureDir();
469
+ feishuWs = new FeishuWSClient({ appId: config.FEISHU_APP_ID, appSecret: config.FEISHU_APP_SECRET }, {
470
+ onMessage: async (msg) => {
471
+ messageQueue.enqueue(msg);
472
+ },
473
+ mediaDownloader,
474
+ client: larkClient,
475
+ db,
476
+ chatSyncService: chatSyncService ?? undefined,
477
+ feishuSender: feishuSender ?? undefined,
478
+ logger: app.log,
479
+ });
480
+ feishuWs.connect().then(() => {
481
+ app.log.info('飞书 WebSocket 连接已建立');
482
+ }).catch((err) => {
483
+ app.log.error({ err }, '飞书 WebSocket 连接失败');
484
+ });
485
+ }
486
+ else {
487
+ app.log.info('飞书凭证未配置,跳过 WebSocket 连接');
488
+ }
489
+ // ── Graceful shutdown ──
490
+ let shuttingDown = false;
491
+ const shutdown = async (signal) => {
492
+ if (shuttingDown)
493
+ return;
494
+ shuttingDown = true;
495
+ app.log.info(`收到 ${signal},开始优雅关闭...`);
496
+ if (feishuWs)
497
+ await feishuWs.close();
498
+ healthMonitor?.stop();
499
+ messageQueue.dispose();
500
+ await app.close();
501
+ if (loidBrain) {
502
+ try {
503
+ await loidBrain.dispose();
504
+ }
505
+ catch (err) {
506
+ app.log.error({ err }, 'LoidBrain 关闭失败');
507
+ }
508
+ }
509
+ await frankyOrchestrator.shutdown();
510
+ const result = await broker.shutdown(30_000);
511
+ if (result.unfinished.length > 0) {
512
+ app.log.info(`优雅关闭: ${result.unfinished.length} 个实例未完成`);
513
+ }
514
+ app.log.info('Anya server 已关闭');
515
+ };
516
+ process.on('SIGINT', () => { shutdown('SIGINT').then(() => process.exit(0)); });
517
+ process.on('SIGTERM', () => { shutdown('SIGTERM').then(() => process.exit(0)); });
518
+ return { app, config };
519
+ }
520
+ // 仅在直接运行时启动
521
+ const isDirectRun = process.argv[1]?.endsWith('main.js') || process.argv[1]?.endsWith('main.ts');
522
+ if (isDirectRun) {
523
+ startServer().catch((err) => {
524
+ console.error('启动失败:', err);
525
+ process.exit(1);
526
+ });
527
+ }
528
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1,2 @@
1
+ export { TraceContext } from './trace-context.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,92 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { insertTraceSpan, updateTraceSpan } from '@team-anya/db';
3
+ export class TraceContext {
4
+ traceId;
5
+ db;
6
+ logger;
7
+ spanStack = [];
8
+ constructor(config, traceId) {
9
+ this.traceId = traceId ?? randomUUID();
10
+ this.db = config.db;
11
+ this.logger = config.logger;
12
+ }
13
+ startSpan(operation, input) {
14
+ const spanId = randomUUID();
15
+ const parentSpanId = this.spanStack.length > 0
16
+ ? this.spanStack[this.spanStack.length - 1].spanId
17
+ : null;
18
+ this.spanStack.push({ spanId, operation, startedAt: Date.now() });
19
+ // Fire-and-forget DB write
20
+ try {
21
+ insertTraceSpan(this.db, {
22
+ trace_id: this.traceId,
23
+ span_id: spanId,
24
+ parent_span_id: parentSpanId,
25
+ operation,
26
+ status: 'in_progress',
27
+ started_at: new Date().toISOString(),
28
+ input: input !== undefined ? JSON.stringify(input) : null,
29
+ });
30
+ }
31
+ catch (err) {
32
+ this.logger?.error('TraceContext: insertTraceSpan 失败', err);
33
+ }
34
+ return spanId;
35
+ }
36
+ endSpan(opts) {
37
+ const targetSpanId = opts?.spanId;
38
+ let entry;
39
+ if (targetSpanId) {
40
+ // Find and remove the specific span from the stack
41
+ const idx = this.spanStack.findIndex(s => s.spanId === targetSpanId);
42
+ if (idx >= 0) {
43
+ entry = this.spanStack[idx];
44
+ this.spanStack.splice(idx, 1);
45
+ }
46
+ }
47
+ else {
48
+ // Pop the top of the stack
49
+ entry = this.spanStack.pop();
50
+ }
51
+ if (!entry) {
52
+ this.logger?.error('TraceContext: endSpan 未找到对应 span');
53
+ return;
54
+ }
55
+ const endedAt = Date.now();
56
+ const durationMs = endedAt - entry.startedAt;
57
+ try {
58
+ updateTraceSpan(this.db, entry.spanId, {
59
+ status: opts?.status ?? 'success',
60
+ ended_at: new Date().toISOString(),
61
+ duration_ms: durationMs,
62
+ output: opts?.output !== undefined ? JSON.stringify(opts.output) : null,
63
+ error: opts?.error ?? null,
64
+ metadata: opts?.metadata ? JSON.stringify(opts.metadata) : null,
65
+ });
66
+ }
67
+ catch (err) {
68
+ this.logger?.error('TraceContext: updateTraceSpan 失败', err);
69
+ }
70
+ }
71
+ async withSpan(operation, input, fn) {
72
+ const spanId = this.startSpan(operation, input);
73
+ try {
74
+ const result = await fn();
75
+ this.endSpan({
76
+ spanId,
77
+ status: 'success',
78
+ output: result,
79
+ });
80
+ return result;
81
+ }
82
+ catch (err) {
83
+ this.endSpan({
84
+ spanId,
85
+ status: 'error',
86
+ error: err instanceof Error ? err.message : String(err),
87
+ });
88
+ throw err;
89
+ }
90
+ }
91
+ }
92
+ //# sourceMappingURL=trace-context.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=message.js.map