team-anya-cli 0.1.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 (163) hide show
  1. package/README.md +38 -0
  2. package/anya/prompts/execution-guides/git-delivery.md +38 -0
  3. package/anya/prompts/execution-guides/testing-and-self-heal.md +28 -0
  4. package/anya/prompts/protocols/brief-assembly.md +55 -0
  5. package/anya/prompts/protocols/report.md +175 -0
  6. package/anya/prompts/protocols/review.md +90 -0
  7. package/anya/prompts/task-claude-md.template.md +32 -0
  8. package/apps/server/dist/broker/cc-broker.js +257 -0
  9. package/apps/server/dist/cli.js +296 -0
  10. package/apps/server/dist/config.js +76 -0
  11. package/apps/server/dist/daemon.js +51 -0
  12. package/apps/server/dist/gateway/chat-sync.js +135 -0
  13. package/apps/server/dist/gateway/command-router.js +114 -0
  14. package/apps/server/dist/gateway/commands/cancel.js +32 -0
  15. package/apps/server/dist/gateway/commands/help.js +16 -0
  16. package/apps/server/dist/gateway/commands/index.js +26 -0
  17. package/apps/server/dist/gateway/commands/restart.js +34 -0
  18. package/apps/server/dist/gateway/commands/status.js +34 -0
  19. package/apps/server/dist/gateway/commands/tasks.js +33 -0
  20. package/apps/server/dist/gateway/feishu-sender.js +346 -0
  21. package/apps/server/dist/gateway/feishu-ws.js +254 -0
  22. package/apps/server/dist/gateway/http.js +994 -0
  23. package/apps/server/dist/gateway/media-downloader.js +149 -0
  24. package/apps/server/dist/gateway/message-events.js +10 -0
  25. package/apps/server/dist/gateway/message-intake.js +50 -0
  26. package/apps/server/dist/gateway/message-queue.js +104 -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 +104 -0
  30. package/apps/server/dist/loid/clarifier.js +162 -0
  31. package/apps/server/dist/loid/context-builder.js +413 -0
  32. package/apps/server/dist/loid/mcp-server.js +104 -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/reporter.js +148 -0
  37. package/apps/server/dist/loid/schemas.js +117 -0
  38. package/apps/server/dist/loid/self-calibrator.js +314 -0
  39. package/apps/server/dist/loid/session-manager.js +217 -0
  40. package/apps/server/dist/loid/session.js +271 -0
  41. package/apps/server/dist/loid/worktree-manager.js +191 -0
  42. package/apps/server/dist/main.js +337 -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 +104 -0
  47. package/apps/server/dist/yor/yor-orchestrator.js +233 -0
  48. package/apps/web/dist/assets/index-CHIT0Dya.css +1 -0
  49. package/apps/web/dist/assets/index-CJzAjoVH.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 +664 -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 +59 -0
  56. package/packages/core/dist/errors.js +35 -0
  57. package/packages/core/dist/index.js +7 -0
  58. package/packages/core/dist/office-init.js +97 -0
  59. package/packages/core/dist/scope/checker.js +114 -0
  60. package/packages/core/dist/scope/defaults.js +40 -0
  61. package/packages/core/dist/scope/index.js +3 -0
  62. package/packages/core/dist/state-machine.js +85 -0
  63. package/packages/core/dist/types/audit.js +12 -0
  64. package/packages/core/dist/types/backend.js +2 -0
  65. package/packages/core/dist/types/commitment.js +17 -0
  66. package/packages/core/dist/types/communication.js +18 -0
  67. package/packages/core/dist/types/index.js +8 -0
  68. package/packages/core/dist/types/opportunity.js +27 -0
  69. package/packages/core/dist/types/org.js +26 -0
  70. package/packages/core/dist/types/task.js +46 -0
  71. package/packages/core/package.json +10 -0
  72. package/packages/db/dist/client.js +69 -0
  73. package/packages/db/dist/index.js +603 -0
  74. package/packages/db/dist/schema/audit-events.js +13 -0
  75. package/packages/db/dist/schema/cc-sessions.js +14 -0
  76. package/packages/db/dist/schema/chats.js +33 -0
  77. package/packages/db/dist/schema/commitments.js +18 -0
  78. package/packages/db/dist/schema/communication-events.js +14 -0
  79. package/packages/db/dist/schema/index.js +12 -0
  80. package/packages/db/dist/schema/message-log.js +20 -0
  81. package/packages/db/dist/schema/opportunities.js +23 -0
  82. package/packages/db/dist/schema/org.js +36 -0
  83. package/packages/db/dist/schema/projects.js +23 -0
  84. package/packages/db/dist/schema/tasks.js +46 -0
  85. package/packages/db/dist/schema/trace-spans.js +19 -0
  86. package/packages/db/package.json +12 -0
  87. package/packages/db/src/migrations/0000_simple_magneto.sql +148 -0
  88. package/packages/db/src/migrations/0001_nifty_morph.sql +42 -0
  89. package/packages/db/src/migrations/0002_common_joshua_kane.sql +20 -0
  90. package/packages/db/src/migrations/0003_add_cc_sessions.sql +13 -0
  91. package/packages/db/src/migrations/0004_jittery_triathlon.sql +1 -0
  92. package/packages/db/src/migrations/meta/0000_snapshot.json +987 -0
  93. package/packages/db/src/migrations/meta/0001_snapshot.json +1280 -0
  94. package/packages/db/src/migrations/meta/0002_snapshot.json +1417 -0
  95. package/packages/db/src/migrations/meta/0004_snapshot.json +1505 -0
  96. package/packages/db/src/migrations/meta/_journal.json +41 -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/loid/decision-log.js +15 -0
  117. package/packages/mcp-tools/dist/layer2/loid/decision-no-action.js +15 -0
  118. package/packages/mcp-tools/dist/layer2/loid/delivery-create-pr.js +30 -0
  119. package/packages/mcp-tools/dist/layer2/loid/delivery-share.js +12 -0
  120. package/packages/mcp-tools/dist/layer2/loid/delivery-submit.js +77 -0
  121. package/packages/mcp-tools/dist/layer2/loid/delivery-upload.js +18 -0
  122. package/packages/mcp-tools/dist/layer2/loid/project-remove.js +16 -0
  123. package/packages/mcp-tools/dist/layer2/loid/project-upsert.js +33 -0
  124. package/packages/mcp-tools/dist/layer2/loid/task-dispatch.js +177 -0
  125. package/packages/mcp-tools/dist/layer2/loid/task-lookup.js +38 -0
  126. package/packages/mcp-tools/dist/layer2/loid/yor-approve.js +8 -0
  127. package/packages/mcp-tools/dist/layer2/loid/yor-kill.js +7 -0
  128. package/packages/mcp-tools/dist/layer2/loid/yor-rework.js +7 -0
  129. package/packages/mcp-tools/dist/layer2/loid/yor-spawn.js +15 -0
  130. package/packages/mcp-tools/dist/layer2/loid/yor-status.js +8 -0
  131. package/packages/mcp-tools/dist/layer2/yor/task-block.js +11 -0
  132. package/packages/mcp-tools/dist/layer2/yor/task-deliver.js +35 -0
  133. package/packages/mcp-tools/dist/layer2/yor/task-progress.js +21 -0
  134. package/packages/mcp-tools/dist/layer3/adapters/feishu-adapter.js +191 -0
  135. package/packages/mcp-tools/dist/layer3/adapters/types.js +28 -0
  136. package/packages/mcp-tools/dist/layer3/channel-receive.js +11 -0
  137. package/packages/mcp-tools/dist/layer3/channel-send.js +90 -0
  138. package/packages/mcp-tools/dist/layer3/file-upload.js +44 -0
  139. package/packages/mcp-tools/dist/registry.js +779 -0
  140. package/packages/mcp-tools/package.json +13 -0
  141. package/workspace/.claude/settings.local.json +9 -0
  142. package/workspace/.mcp.json +12 -0
  143. package/workspace/CHARTER.md +73 -0
  144. package/workspace/CLAUDE.md +49 -0
  145. package/workspace/PROTOCOL.md +126 -0
  146. package/workspace/TOOLS.md +464 -0
  147. package/workspace/audit/.gitkeep +0 -0
  148. package/workspace/loid/CLAUDE.md +12 -0
  149. package/workspace/loid/PLAYBOOK.md +198 -0
  150. package/workspace/loid/PROFILE.md +78 -0
  151. package/workspace/memory/commitments/.gitkeep +0 -0
  152. package/workspace/memory/execution/.gitkeep +0 -0
  153. package/workspace/memory/people/.gitkeep +0 -0
  154. package/workspace/memory/projects/.gitkeep +0 -0
  155. package/workspace/memory/self/.gitkeep +0 -0
  156. package/workspace/reference/identity/.gitkeep +0 -0
  157. package/workspace/reference/org/escalation.yaml +24 -0
  158. package/workspace/reference/org/ownership.yaml +28 -0
  159. package/workspace/reports/.gitkeep +0 -0
  160. package/workspace/yor/CLAUDE.md +22 -0
  161. package/workspace/yor/PLAYBOOK.md +73 -0
  162. package/workspace/yor/PROFILE.md +52 -0
  163. package/workspace/yor/SELF-HEAL.md +39 -0
@@ -0,0 +1,149 @@
1
+ import { writeFile, mkdir, readFile, unlink } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ /**
5
+ * 飞书媒体文件下载器
6
+ *
7
+ * 负责将飞书消息中的图片/文件/音频/视频下载到本地磁盘。
8
+ * 下载失败时返回 null,不阻断消息处理。
9
+ */
10
+ export class MediaDownloader {
11
+ client;
12
+ mediaDir;
13
+ constructor(client, config) {
14
+ this.client = client;
15
+ this.mediaDir = config.mediaDir;
16
+ }
17
+ /** 确保媒体目录存在 */
18
+ async ensureDir() {
19
+ await mkdir(this.mediaDir, { recursive: true });
20
+ }
21
+ /**
22
+ * 下载飞书媒体文件到本地
23
+ *
24
+ * @param messageId 飞书消息 ID(图片下载需要)
25
+ * @param msgType 消息类型:image | file | audio | video
26
+ * @param contentJson 解析后的消息内容 JSON
27
+ * @returns MediaAttachment 或 null(下载失败时)
28
+ */
29
+ async download(messageId, msgType, contentJson) {
30
+ try {
31
+ let data = null;
32
+ let filename = null;
33
+ let sourceKey = '';
34
+ if (msgType === 'image') {
35
+ const result = await this.downloadImage(messageId, contentJson);
36
+ if (!result)
37
+ return null;
38
+ ({ data, filename, sourceKey } = result);
39
+ }
40
+ else if (msgType === 'file' || msgType === 'audio' || msgType === 'video') {
41
+ const result = await this.downloadFile(messageId, msgType, contentJson);
42
+ if (!result)
43
+ return null;
44
+ ({ data, filename, sourceKey } = result);
45
+ }
46
+ else {
47
+ return null;
48
+ }
49
+ if (!data || !filename)
50
+ return null;
51
+ const safeFilename = `${messageId}_${filename}`.replace(/[/\\:*?"<>|]/g, '_');
52
+ const localPath = join(this.mediaDir, safeFilename);
53
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
54
+ await writeFile(localPath, buffer);
55
+ return {
56
+ localPath,
57
+ mediaType: msgType,
58
+ originalName: filename,
59
+ sourceKey,
60
+ size: buffer.length,
61
+ };
62
+ }
63
+ catch (err) {
64
+ console.error(`[media-downloader] 下载 ${msgType} 失败:`, err);
65
+ return null;
66
+ }
67
+ }
68
+ /**
69
+ * 从飞书 SDK 文件下载响应中提取 Buffer
70
+ *
71
+ * 飞书 SDK 返回 { writeFile, getReadableStream, headers },不是直接的 Buffer。
72
+ * 优先用 getReadableStream 流式读取,fallback 到 writeFile 临时文件。
73
+ */
74
+ async extractBuffer(resp) {
75
+ const r = resp;
76
+ // 方式 1:通过 getReadableStream 流式读取(首选,不需要临时文件)
77
+ if (typeof r.getReadableStream === 'function') {
78
+ try {
79
+ const stream = r.getReadableStream();
80
+ const chunks = [];
81
+ for await (const chunk of stream) {
82
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
83
+ }
84
+ if (chunks.length > 0)
85
+ return Buffer.concat(chunks);
86
+ }
87
+ catch {
88
+ // stream 读取失败,fallthrough 到 writeFile
89
+ }
90
+ }
91
+ // 方式 2:通过 writeFile 写入临时文件再读取
92
+ if (typeof r.writeFile === 'function') {
93
+ const tmpPath = join(tmpdir(), `anya-media-${Date.now()}-${Math.random().toString(36).slice(2)}`);
94
+ try {
95
+ await r.writeFile(tmpPath);
96
+ const data = await readFile(tmpPath);
97
+ await unlink(tmpPath).catch(() => { });
98
+ if (data.length > 0)
99
+ return data;
100
+ }
101
+ catch {
102
+ await unlink(tmpPath).catch(() => { });
103
+ }
104
+ }
105
+ // 方式 3:兼容旧版本 SDK 可能直接返回 Buffer 的情况
106
+ if (Buffer.isBuffer(r))
107
+ return r;
108
+ if (r instanceof Uint8Array)
109
+ return Buffer.from(r);
110
+ const rawData = r.data;
111
+ if (Buffer.isBuffer(rawData))
112
+ return rawData;
113
+ if (rawData instanceof Uint8Array)
114
+ return Buffer.from(rawData);
115
+ return null;
116
+ }
117
+ async downloadImage(messageId, contentJson) {
118
+ const imageKey = contentJson.image_key;
119
+ if (!imageKey || !messageId)
120
+ return null;
121
+ const resp = await this.client.im.messageResource.get({
122
+ path: { message_id: messageId, file_key: imageKey },
123
+ params: { type: 'image' },
124
+ });
125
+ const data = await this.extractBuffer(resp);
126
+ if (!data)
127
+ return null;
128
+ const filename = resp?.fileName ?? `${imageKey.slice(0, 16)}.jpg`;
129
+ return { data, filename, sourceKey: imageKey };
130
+ }
131
+ async downloadFile(messageId, msgType, contentJson) {
132
+ const fileKey = contentJson.file_key;
133
+ if (!fileKey || !messageId)
134
+ return null;
135
+ const resp = await this.client.im.messageResource.get({
136
+ path: { message_id: messageId, file_key: fileKey },
137
+ params: { type: msgType === 'file' ? 'file' : msgType },
138
+ });
139
+ const data = await this.extractBuffer(resp);
140
+ if (!data)
141
+ return null;
142
+ const ext = msgType === 'audio' ? '.opus' : (msgType === 'video' ? '.mp4' : '');
143
+ const filename = resp?.fileName
144
+ ?? contentJson.file_name
145
+ ?? `${fileKey.slice(0, 16)}${ext}`;
146
+ return { data, filename, sourceKey: fileKey };
147
+ }
148
+ }
149
+ //# sourceMappingURL=media-downloader.js.map
@@ -0,0 +1,10 @@
1
+ import { EventEmitter } from 'node:events';
2
+ /**
3
+ * 全局消息事件总线
4
+ *
5
+ * message-intake 和 feishu-sender 写入 message_log 后 emit 'message' 事件,
6
+ * http.ts 的 SSE 端点监听该事件推送给客户端。
7
+ */
8
+ export const messageEvents = new EventEmitter();
9
+ messageEvents.setMaxListeners(100);
10
+ //# sourceMappingURL=message-events.js.map
@@ -0,0 +1,50 @@
1
+ import { insertMessageLog } from '@team-anya/db';
2
+ export class MessageIntake {
3
+ deps;
4
+ constructor(deps) {
5
+ this.deps = deps;
6
+ }
7
+ /**
8
+ * 处理新消息:最小检测 + 透传给 Loid
9
+ */
10
+ async handle(message) {
11
+ const logger = this.deps.logger;
12
+ // 1. 记录到 DB(审计用)
13
+ try {
14
+ const metadataObj = {
15
+ ...message.metadata,
16
+ ...(message.media.length > 0 ? { media: message.media } : {}),
17
+ ...(message.sender ? { sender_open_id: message.sender } : {}),
18
+ };
19
+ insertMessageLog(this.deps.db, {
20
+ chat_id: message.chatId ?? null,
21
+ sender: message.senderName ?? message.sender ?? null,
22
+ content: message.content,
23
+ direction: 'inbound',
24
+ source_type: message.metadata?.source_type ?? 'unknown',
25
+ source_ref: message.metadata?.message_id,
26
+ trace_id: message.metadata?._traceId,
27
+ metadata: Object.keys(metadataObj).length > 0
28
+ ? JSON.stringify(metadataObj)
29
+ : null,
30
+ });
31
+ }
32
+ catch (err) {
33
+ logger?.error('[MessageIntake] 记录消息失败:', err);
34
+ }
35
+ // 1.5 命令拦截(在构建上下文之前)
36
+ if (this.deps.commandRouter) {
37
+ const handled = await this.deps.commandRouter.tryHandle(message);
38
+ if (handled) {
39
+ logger?.info(`[MessageIntake] 命令已拦截处理,跳过 Loid`);
40
+ return;
41
+ }
42
+ }
43
+ // 2. 构建上下文(含产品/项目注册表)
44
+ const context = await this.deps.contextBuilder.buildMessageContext(message);
45
+ // 3. 透传给 Loid(完全自主判断)
46
+ logger?.info(`[MessageIntake] 转发消息给 Loid: ${message.sender}@${message.chatId ?? 'DM'}`);
47
+ await this.deps.loidBrain.handleNewMessage(context);
48
+ }
49
+ }
50
+ //# sourceMappingURL=message-intake.js.map
@@ -0,0 +1,104 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ // ── EventDedup ──
3
+ class EventDedup {
4
+ ttlMs;
5
+ seen = new Map();
6
+ timer;
7
+ constructor(ttlMs) {
8
+ this.ttlMs = ttlMs;
9
+ this.timer = setInterval(() => this.cleanup(), ttlMs);
10
+ }
11
+ isDuplicate(eventId) {
12
+ if (this.seen.has(eventId)) {
13
+ return true;
14
+ }
15
+ this.seen.set(eventId, Date.now());
16
+ return false;
17
+ }
18
+ get size() {
19
+ return this.seen.size;
20
+ }
21
+ dispose() {
22
+ clearInterval(this.timer);
23
+ this.seen.clear();
24
+ }
25
+ cleanup() {
26
+ const now = Date.now();
27
+ for (const [id, ts] of this.seen) {
28
+ if (now - ts > this.ttlMs) {
29
+ this.seen.delete(id);
30
+ }
31
+ }
32
+ }
33
+ }
34
+ export class MessageQueue {
35
+ handler;
36
+ dedup;
37
+ sources = new Map();
38
+ logger;
39
+ constructor(config) {
40
+ this.handler = config.handler;
41
+ this.dedup = new EventDedup(config.dedupTtlMs ?? 5 * 60 * 1000);
42
+ this.logger = config.logger ?? { info: console.log, error: console.error };
43
+ }
44
+ enqueue(msg) {
45
+ const eventId = msg.metadata?.event_id;
46
+ if (eventId && this.dedup.isDuplicate(eventId)) {
47
+ // 重复事件,静默跳过
48
+ return;
49
+ }
50
+ const key = this.deriveSourceKey(msg);
51
+ let sq = this.sources.get(key);
52
+ if (!sq) {
53
+ sq = { pending: [], processing: false };
54
+ this.sources.set(key, sq);
55
+ }
56
+ sq.pending.push(msg);
57
+ if (!sq.processing) {
58
+ this.processQueue(key, sq);
59
+ }
60
+ }
61
+ getQueueStats() {
62
+ let totalPending = 0;
63
+ for (const sq of this.sources.values()) {
64
+ totalPending += sq.pending.length;
65
+ }
66
+ return {
67
+ sourceCount: this.sources.size,
68
+ totalPending,
69
+ dedupCacheSize: this.dedup.size,
70
+ };
71
+ }
72
+ dispose() {
73
+ this.dedup.dispose();
74
+ }
75
+ deriveSourceKey(msg) {
76
+ if (msg.chatId && !msg.isDirectMessage) {
77
+ return msg.chatId;
78
+ }
79
+ if (msg.sender) {
80
+ return msg.sender;
81
+ }
82
+ return 'default';
83
+ }
84
+ async processQueue(key, sq) {
85
+ sq.processing = true;
86
+ while (sq.pending.length > 0) {
87
+ const current = sq.pending.shift();
88
+ // 为每条消息注入 traceId
89
+ if (!current.metadata)
90
+ current.metadata = {};
91
+ if (!current.metadata._traceId) {
92
+ current.metadata._traceId = randomUUID();
93
+ }
94
+ try {
95
+ await this.handler(current);
96
+ }
97
+ catch (err) {
98
+ this.logger.error('[MessageQueue] handler 异常:', err);
99
+ }
100
+ }
101
+ sq.processing = false;
102
+ }
103
+ }
104
+ //# sourceMappingURL=message-queue.js.map
@@ -0,0 +1,142 @@
1
+ /**
2
+ * CC Session JSONL 文件读取器
3
+ *
4
+ * 从 ~/.claude/projects/{encoded-path}/{sessionId}.jsonl 读取会话数据。
5
+ * 参考 reference-projects/claude-run/api/storage.ts 适配。
6
+ */
7
+ import { readFileSync, existsSync, readdirSync, statSync, openSync, readSync, closeSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { homedir } from 'node:os';
10
+ // ── 核心函数 ──
11
+ /**
12
+ * 编码项目路径为 Claude 的目录名
13
+ * 与 Claude Code 内部逻辑一致:将路径分隔符替换为 `-`
14
+ */
15
+ function encodeProjectPath(projectPath) {
16
+ // Claude Code 使用 encodeURIComponent 风格但实际更简单:
17
+ // 去掉开头 /,把 / 替换为 -
18
+ return projectPath.replace(/^\//, '').replace(/\//g, '-');
19
+ }
20
+ /**
21
+ * 获取 Claude 数据目录
22
+ */
23
+ function getClaudeDir() {
24
+ return join(homedir(), '.claude');
25
+ }
26
+ /**
27
+ * 查找 session JSONL 文件
28
+ */
29
+ export function findSessionFile(sessionId, projectPath) {
30
+ const claudeDir = getClaudeDir();
31
+ const projectsDir = join(claudeDir, 'projects');
32
+ if (!existsSync(projectsDir))
33
+ return null;
34
+ // 如果指定了项目路径,直接在对应目录查找
35
+ if (projectPath) {
36
+ const encoded = encodeProjectPath(projectPath);
37
+ const filePath = join(projectsDir, encoded, `${sessionId}.jsonl`);
38
+ if (existsSync(filePath))
39
+ return filePath;
40
+ }
41
+ // 遍历所有项目目录查找
42
+ try {
43
+ const dirs = readdirSync(projectsDir, { withFileTypes: true });
44
+ for (const dir of dirs) {
45
+ if (!dir.isDirectory())
46
+ continue;
47
+ const filePath = join(projectsDir, dir.name, `${sessionId}.jsonl`);
48
+ if (existsSync(filePath))
49
+ return filePath;
50
+ }
51
+ }
52
+ catch {
53
+ // ignore
54
+ }
55
+ return null;
56
+ }
57
+ /**
58
+ * 读取 session JSONL 文件并解析为消息数组
59
+ */
60
+ export function readSessionConversation(sessionId, projectPath) {
61
+ const filePath = findSessionFile(sessionId, projectPath);
62
+ if (!filePath)
63
+ return [];
64
+ try {
65
+ const content = readFileSync(filePath, 'utf-8');
66
+ const lines = content.split('\n').filter(line => line.trim());
67
+ const messages = [];
68
+ for (const line of lines) {
69
+ try {
70
+ const entry = JSON.parse(line);
71
+ // JSONL 条目可能是完整消息或包装在 message 字段中
72
+ if (entry.type === 'user' || entry.type === 'assistant') {
73
+ messages.push(entry);
74
+ }
75
+ else if (entry.type === 'summary') {
76
+ messages.push(entry);
77
+ }
78
+ }
79
+ catch {
80
+ // 跳过无法解析的行
81
+ }
82
+ }
83
+ return messages;
84
+ }
85
+ catch {
86
+ return [];
87
+ }
88
+ }
89
+ /**
90
+ * 从指定 byte offset 开始增量读取 JSONL 文件
91
+ * 参考 claude-run/api/storage.ts getConversationStream
92
+ */
93
+ export function readSessionStream(sessionId, fromOffset = 0, projectPath) {
94
+ const filePath = findSessionFile(sessionId, projectPath);
95
+ if (!filePath)
96
+ return { messages: [], nextOffset: 0 };
97
+ try {
98
+ const fileStat = statSync(filePath);
99
+ const fileSize = fileStat.size;
100
+ if (fromOffset >= fileSize) {
101
+ return { messages: [], nextOffset: fromOffset };
102
+ }
103
+ const bufSize = fileSize - fromOffset;
104
+ const buf = Buffer.alloc(bufSize);
105
+ const fd = openSync(filePath, 'r');
106
+ try {
107
+ readSync(fd, buf, 0, bufSize, fromOffset);
108
+ }
109
+ finally {
110
+ closeSync(fd);
111
+ }
112
+ const chunk = buf.toString('utf-8');
113
+ const lines = chunk.split('\n');
114
+ const messages = [];
115
+ let bytesConsumed = 0;
116
+ for (const line of lines) {
117
+ const lineBytes = Buffer.byteLength(line, 'utf-8') + 1; // +1 for \n
118
+ if (line.trim()) {
119
+ try {
120
+ const entry = JSON.parse(line);
121
+ if (entry.type === 'user' || entry.type === 'assistant' || entry.type === 'summary') {
122
+ messages.push(entry);
123
+ }
124
+ bytesConsumed += lineBytes;
125
+ }
126
+ catch {
127
+ // 不完整的行(文件还在写入),停止读取
128
+ break;
129
+ }
130
+ }
131
+ else {
132
+ bytesConsumed += lineBytes;
133
+ }
134
+ }
135
+ const nextOffset = fromOffset + bytesConsumed;
136
+ return { messages, nextOffset: nextOffset > fileSize ? fileSize : nextOffset };
137
+ }
138
+ catch {
139
+ return { messages: [], nextOffset: fromOffset };
140
+ }
141
+ }
142
+ //# sourceMappingURL=session-reader.js.map
@@ -0,0 +1,115 @@
1
+ import { WebSocketServer } from 'ws';
2
+ export class WsPushServer {
3
+ wss = null;
4
+ config;
5
+ logger;
6
+ heartbeatTimer = null;
7
+ clients = new Map();
8
+ constructor(config, logger) {
9
+ this.config = config;
10
+ this.logger = logger ?? { info: console.log, error: console.error, warn: console.warn };
11
+ }
12
+ /**
13
+ * 启动 WebSocket Server
14
+ */
15
+ start() {
16
+ return new Promise((resolve, reject) => {
17
+ this.wss = new WebSocketServer({ port: this.config.port });
18
+ this.wss.on('listening', () => {
19
+ this.logger.info(`WsPushServer 已启动,监听端口 ${this.config.port}`);
20
+ this.startHeartbeat();
21
+ resolve();
22
+ });
23
+ this.wss.on('error', (err) => {
24
+ this.logger.error('WsPushServer 错误:', err);
25
+ reject(err);
26
+ });
27
+ this.wss.on('connection', (ws) => {
28
+ this.onConnection(ws);
29
+ });
30
+ });
31
+ }
32
+ /**
33
+ * 关闭 WebSocket Server
34
+ */
35
+ async stop() {
36
+ this.stopHeartbeat();
37
+ if (!this.wss)
38
+ return;
39
+ // 关闭所有客户端连接
40
+ for (const [ws] of this.clients) {
41
+ ws.terminate();
42
+ }
43
+ this.clients.clear();
44
+ return new Promise((resolve) => {
45
+ this.wss.close(() => {
46
+ this.wss = null;
47
+ this.logger.info('WsPushServer 已关闭');
48
+ resolve();
49
+ });
50
+ });
51
+ }
52
+ /**
53
+ * 广播事件给所有连接的客户端
54
+ */
55
+ broadcast(event) {
56
+ const message = JSON.stringify(event);
57
+ for (const [ws] of this.clients) {
58
+ if (ws.readyState === 1 /* WebSocket.OPEN */) {
59
+ ws.send(message, (err) => {
60
+ if (err) {
61
+ this.logger.error('广播消息失败:', err);
62
+ }
63
+ });
64
+ }
65
+ }
66
+ }
67
+ /**
68
+ * 获取当前连接的客户端数量
69
+ */
70
+ getClientCount() {
71
+ return this.clients.size;
72
+ }
73
+ onConnection(ws) {
74
+ const meta = { isAlive: true };
75
+ this.clients.set(ws, meta);
76
+ ws.on('pong', () => {
77
+ meta.isAlive = true;
78
+ });
79
+ ws.on('close', () => {
80
+ this.clients.delete(ws);
81
+ });
82
+ ws.on('error', (err) => {
83
+ this.logger.error('WsPush 客户端错误:', err);
84
+ this.clients.delete(ws);
85
+ });
86
+ }
87
+ /**
88
+ * 启动心跳检测
89
+ */
90
+ startHeartbeat() {
91
+ const intervalMs = this.config.heartbeatIntervalMs ?? 30_000;
92
+ this.heartbeatTimer = setInterval(() => {
93
+ for (const [ws, meta] of this.clients) {
94
+ if (!meta.isAlive) {
95
+ this.logger.warn('心跳超时,断开客户端');
96
+ ws.terminate();
97
+ this.clients.delete(ws);
98
+ continue;
99
+ }
100
+ meta.isAlive = false;
101
+ ws.ping();
102
+ }
103
+ }, intervalMs);
104
+ }
105
+ /**
106
+ * 停止心跳检测
107
+ */
108
+ stopHeartbeat() {
109
+ if (this.heartbeatTimer) {
110
+ clearInterval(this.heartbeatTimer);
111
+ this.heartbeatTimer = null;
112
+ }
113
+ }
114
+ }
115
+ //# sourceMappingURL=ws-push.js.map
@@ -0,0 +1,104 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { startLoidMcpServer } from './mcp-server.js';
4
+ import { LoidSessionManager } from './session-manager.js';
5
+ // ── LoidBrain ──
6
+ export class LoidBrain {
7
+ config;
8
+ deps;
9
+ ready = false;
10
+ mcpUrl = null;
11
+ mcpClose;
12
+ protocols = {};
13
+ sessionManager = null;
14
+ constructor(config, deps) {
15
+ this.config = config;
16
+ this.deps = deps;
17
+ }
18
+ async init() {
19
+ // 1. 启动指挥型 MCP server
20
+ const mcp = await startLoidMcpServer(this.deps.mcpDeps);
21
+ this.mcpClose = mcp.close;
22
+ this.mcpUrl = mcp.url;
23
+ // 2. 预读协议文件到内存
24
+ this.loadProtocols();
25
+ // 3. 验证 workingDir 存在
26
+ if (!existsSync(this.config.loidWorkDir)) {
27
+ this.config.logger?.error(`loidWorkDir 不存在: ${this.config.loidWorkDir}`);
28
+ }
29
+ // 4. 初始化 SessionManager
30
+ const logDir = join(this.config.loidWorkDir, '..', '..', 'data', 'logs', 'cc-logs');
31
+ this.sessionManager = new LoidSessionManager({
32
+ broker: this.deps.broker,
33
+ backendConfig: {
34
+ type: 'claude-code',
35
+ binary: this.config.binary,
36
+ workingDir: this.config.loidWorkDir,
37
+ dangerouslySkipPermissions: true,
38
+ maxTurns: this.config.maxTurnsPerSession ?? 50,
39
+ mcpServers: [{ name: 'loid-tools', url: this.mcpUrl }],
40
+ },
41
+ idleTimeoutMs: this.config.idleTimeoutMs,
42
+ maxTurnsPerSession: this.config.maxTurnsPerSession,
43
+ protocols: this.protocols,
44
+ logDir,
45
+ logger: this.config.logger,
46
+ }, {
47
+ db: this.deps.mcpDeps.db,
48
+ });
49
+ this.ready = true;
50
+ this.config.logger?.info('LoidBrain 已初始化 (Session 模式)');
51
+ }
52
+ /**
53
+ * 仅释放所有 Loid 会话(重启场景用),保留 MCP server 和 ready 状态
54
+ */
55
+ async disposeSessions() {
56
+ await this.sessionManager?.dispose();
57
+ this.config.logger?.info('LoidBrain 会话已全部释放(MCP server 保留)');
58
+ }
59
+ async dispose() {
60
+ await this.sessionManager?.dispose();
61
+ await this.mcpClose?.();
62
+ this.ready = false;
63
+ this.config.logger?.info('LoidBrain 已关闭');
64
+ }
65
+ /**
66
+ * 入口 1: 处理新消息
67
+ * 委托给 SessionManager 管理长驻会话
68
+ */
69
+ async handleNewMessage(ctx) {
70
+ this.ensureReady();
71
+ const chatId = ctx.message.chatId ?? ctx.message.sender ?? 'default';
72
+ const sender = ctx.message.sender ?? '未知';
73
+ this.config.logger?.info(`[anya:pipeline] [Loid] 收到消息 ${sender}@${chatId}: "${ctx.message.content.slice(0, 60)}"`);
74
+ await this.sessionManager.handleMessage(chatId, ctx);
75
+ }
76
+ /**
77
+ * 入口 2: 处理 Yor 交付
78
+ * 委托给 SessionManager 管理验收会话
79
+ */
80
+ async handleDelivery(ctx) {
81
+ this.ensureReady();
82
+ this.config.logger?.info(`[anya:pipeline] [Loid] 开始验收 ${ctx.taskId} (exit=${ctx.exitCode})`);
83
+ await this.sessionManager.handleDelivery(ctx);
84
+ }
85
+ // ── 私有方法 ──
86
+ ensureReady() {
87
+ if (!this.ready) {
88
+ throw new Error('LoidBrain 尚未初始化,请先调用 init()');
89
+ }
90
+ }
91
+ loadProtocols() {
92
+ const protocolNames = ['review', 'report'];
93
+ for (const name of protocolNames) {
94
+ const filePath = join(this.config.protocolsDir, `${name}.md`);
95
+ if (existsSync(filePath)) {
96
+ this.protocols[name] = readFileSync(filePath, 'utf-8');
97
+ }
98
+ else {
99
+ this.config.logger?.error?.(`协议文件不存在: ${filePath}`);
100
+ }
101
+ }
102
+ }
103
+ }
104
+ //# sourceMappingURL=brain.js.map