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.
- package/README.md +38 -0
- package/anya/prompts/execution-guides/git-delivery.md +38 -0
- package/anya/prompts/execution-guides/testing-and-self-heal.md +28 -0
- package/anya/prompts/protocols/brief-assembly.md +55 -0
- package/anya/prompts/protocols/report.md +175 -0
- package/anya/prompts/protocols/review.md +90 -0
- package/anya/prompts/task-claude-md.template.md +32 -0
- package/apps/server/dist/broker/cc-broker.js +257 -0
- package/apps/server/dist/cli.js +296 -0
- package/apps/server/dist/config.js +76 -0
- package/apps/server/dist/daemon.js +51 -0
- package/apps/server/dist/gateway/chat-sync.js +135 -0
- package/apps/server/dist/gateway/command-router.js +114 -0
- package/apps/server/dist/gateway/commands/cancel.js +32 -0
- package/apps/server/dist/gateway/commands/help.js +16 -0
- package/apps/server/dist/gateway/commands/index.js +26 -0
- package/apps/server/dist/gateway/commands/restart.js +34 -0
- package/apps/server/dist/gateway/commands/status.js +34 -0
- package/apps/server/dist/gateway/commands/tasks.js +33 -0
- package/apps/server/dist/gateway/feishu-sender.js +346 -0
- package/apps/server/dist/gateway/feishu-ws.js +254 -0
- package/apps/server/dist/gateway/http.js +994 -0
- package/apps/server/dist/gateway/media-downloader.js +149 -0
- package/apps/server/dist/gateway/message-events.js +10 -0
- package/apps/server/dist/gateway/message-intake.js +50 -0
- package/apps/server/dist/gateway/message-queue.js +104 -0
- package/apps/server/dist/gateway/session-reader.js +142 -0
- package/apps/server/dist/gateway/ws-push.js +115 -0
- package/apps/server/dist/loid/brain.js +104 -0
- package/apps/server/dist/loid/clarifier.js +162 -0
- package/apps/server/dist/loid/context-builder.js +413 -0
- package/apps/server/dist/loid/mcp-server.js +104 -0
- package/apps/server/dist/loid/memory-settler.js +189 -0
- package/apps/server/dist/loid/opportunity-manager.js +148 -0
- package/apps/server/dist/loid/profile-updater.js +179 -0
- package/apps/server/dist/loid/reporter.js +148 -0
- package/apps/server/dist/loid/schemas.js +117 -0
- package/apps/server/dist/loid/self-calibrator.js +314 -0
- package/apps/server/dist/loid/session-manager.js +217 -0
- package/apps/server/dist/loid/session.js +271 -0
- package/apps/server/dist/loid/worktree-manager.js +191 -0
- package/apps/server/dist/main.js +337 -0
- package/apps/server/dist/tracing/index.js +2 -0
- package/apps/server/dist/tracing/trace-context.js +92 -0
- package/apps/server/dist/types/message.js +2 -0
- package/apps/server/dist/yor/yor-mcp-server.js +104 -0
- package/apps/server/dist/yor/yor-orchestrator.js +233 -0
- package/apps/web/dist/assets/index-CHIT0Dya.css +1 -0
- package/apps/web/dist/assets/index-CJzAjoVH.js +798 -0
- package/apps/web/dist/index.html +13 -0
- package/package.json +42 -0
- package/packages/cc-client/dist/claude-code-backend.js +664 -0
- package/packages/cc-client/dist/index.js +2 -0
- package/packages/cc-client/package.json +11 -0
- package/packages/core/dist/constants.js +59 -0
- package/packages/core/dist/errors.js +35 -0
- package/packages/core/dist/index.js +7 -0
- package/packages/core/dist/office-init.js +97 -0
- package/packages/core/dist/scope/checker.js +114 -0
- package/packages/core/dist/scope/defaults.js +40 -0
- package/packages/core/dist/scope/index.js +3 -0
- package/packages/core/dist/state-machine.js +85 -0
- package/packages/core/dist/types/audit.js +12 -0
- package/packages/core/dist/types/backend.js +2 -0
- package/packages/core/dist/types/commitment.js +17 -0
- package/packages/core/dist/types/communication.js +18 -0
- package/packages/core/dist/types/index.js +8 -0
- package/packages/core/dist/types/opportunity.js +27 -0
- package/packages/core/dist/types/org.js +26 -0
- package/packages/core/dist/types/task.js +46 -0
- package/packages/core/package.json +10 -0
- package/packages/db/dist/client.js +69 -0
- package/packages/db/dist/index.js +603 -0
- package/packages/db/dist/schema/audit-events.js +13 -0
- package/packages/db/dist/schema/cc-sessions.js +14 -0
- package/packages/db/dist/schema/chats.js +33 -0
- package/packages/db/dist/schema/commitments.js +18 -0
- package/packages/db/dist/schema/communication-events.js +14 -0
- package/packages/db/dist/schema/index.js +12 -0
- package/packages/db/dist/schema/message-log.js +20 -0
- package/packages/db/dist/schema/opportunities.js +23 -0
- package/packages/db/dist/schema/org.js +36 -0
- package/packages/db/dist/schema/projects.js +23 -0
- package/packages/db/dist/schema/tasks.js +46 -0
- package/packages/db/dist/schema/trace-spans.js +19 -0
- package/packages/db/package.json +12 -0
- package/packages/db/src/migrations/0000_simple_magneto.sql +148 -0
- package/packages/db/src/migrations/0001_nifty_morph.sql +42 -0
- package/packages/db/src/migrations/0002_common_joshua_kane.sql +20 -0
- package/packages/db/src/migrations/0003_add_cc_sessions.sql +13 -0
- package/packages/db/src/migrations/0004_jittery_triathlon.sql +1 -0
- package/packages/db/src/migrations/meta/0000_snapshot.json +987 -0
- package/packages/db/src/migrations/meta/0001_snapshot.json +1280 -0
- package/packages/db/src/migrations/meta/0002_snapshot.json +1417 -0
- package/packages/db/src/migrations/meta/0004_snapshot.json +1505 -0
- package/packages/db/src/migrations/meta/_journal.json +41 -0
- package/packages/mcp-tools/dist/index.js +41 -0
- package/packages/mcp-tools/dist/layer1/audit-append.js +38 -0
- package/packages/mcp-tools/dist/layer1/audit-query.js +51 -0
- package/packages/mcp-tools/dist/layer1/memory-brief.js +168 -0
- package/packages/mcp-tools/dist/layer1/memory-context.js +124 -0
- package/packages/mcp-tools/dist/layer1/memory-digest.js +126 -0
- package/packages/mcp-tools/dist/layer1/memory-forget.js +108 -0
- package/packages/mcp-tools/dist/layer1/memory-learn.js +63 -0
- package/packages/mcp-tools/dist/layer1/memory-recall.js +287 -0
- package/packages/mcp-tools/dist/layer1/memory-reflect.js +80 -0
- package/packages/mcp-tools/dist/layer1/memory-remember.js +119 -0
- package/packages/mcp-tools/dist/layer1/memory-search.js +263 -0
- package/packages/mcp-tools/dist/layer1/memory-write.js +21 -0
- package/packages/mcp-tools/dist/layer1/org-lookup.js +47 -0
- package/packages/mcp-tools/dist/layer1/project-get.js +28 -0
- package/packages/mcp-tools/dist/layer1/project-list.js +20 -0
- package/packages/mcp-tools/dist/layer1/report-daily.js +68 -0
- package/packages/mcp-tools/dist/layer1/task-get.js +29 -0
- package/packages/mcp-tools/dist/layer1/task-update.js +34 -0
- package/packages/mcp-tools/dist/layer2/loid/decision-log.js +15 -0
- package/packages/mcp-tools/dist/layer2/loid/decision-no-action.js +15 -0
- package/packages/mcp-tools/dist/layer2/loid/delivery-create-pr.js +30 -0
- package/packages/mcp-tools/dist/layer2/loid/delivery-share.js +12 -0
- package/packages/mcp-tools/dist/layer2/loid/delivery-submit.js +77 -0
- package/packages/mcp-tools/dist/layer2/loid/delivery-upload.js +18 -0
- package/packages/mcp-tools/dist/layer2/loid/project-remove.js +16 -0
- package/packages/mcp-tools/dist/layer2/loid/project-upsert.js +33 -0
- package/packages/mcp-tools/dist/layer2/loid/task-dispatch.js +177 -0
- package/packages/mcp-tools/dist/layer2/loid/task-lookup.js +38 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-approve.js +8 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-kill.js +7 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-rework.js +7 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-spawn.js +15 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-status.js +8 -0
- package/packages/mcp-tools/dist/layer2/yor/task-block.js +11 -0
- package/packages/mcp-tools/dist/layer2/yor/task-deliver.js +35 -0
- package/packages/mcp-tools/dist/layer2/yor/task-progress.js +21 -0
- package/packages/mcp-tools/dist/layer3/adapters/feishu-adapter.js +191 -0
- package/packages/mcp-tools/dist/layer3/adapters/types.js +28 -0
- package/packages/mcp-tools/dist/layer3/channel-receive.js +11 -0
- package/packages/mcp-tools/dist/layer3/channel-send.js +90 -0
- package/packages/mcp-tools/dist/layer3/file-upload.js +44 -0
- package/packages/mcp-tools/dist/registry.js +779 -0
- package/packages/mcp-tools/package.json +13 -0
- package/workspace/.claude/settings.local.json +9 -0
- package/workspace/.mcp.json +12 -0
- package/workspace/CHARTER.md +73 -0
- package/workspace/CLAUDE.md +49 -0
- package/workspace/PROTOCOL.md +126 -0
- package/workspace/TOOLS.md +464 -0
- package/workspace/audit/.gitkeep +0 -0
- package/workspace/loid/CLAUDE.md +12 -0
- package/workspace/loid/PLAYBOOK.md +198 -0
- package/workspace/loid/PROFILE.md +78 -0
- package/workspace/memory/commitments/.gitkeep +0 -0
- package/workspace/memory/execution/.gitkeep +0 -0
- package/workspace/memory/people/.gitkeep +0 -0
- package/workspace/memory/projects/.gitkeep +0 -0
- package/workspace/memory/self/.gitkeep +0 -0
- package/workspace/reference/identity/.gitkeep +0 -0
- package/workspace/reference/org/escalation.yaml +24 -0
- package/workspace/reference/org/ownership.yaml +28 -0
- package/workspace/reports/.gitkeep +0 -0
- package/workspace/yor/CLAUDE.md +22 -0
- package/workspace/yor/PLAYBOOK.md +73 -0
- package/workspace/yor/PROFILE.md +52 -0
- package/workspace/yor/SELF-HEAL.md +39 -0
|
@@ -0,0 +1,994 @@
|
|
|
1
|
+
import { createTask, getTask, getAllTasks, getTasksByProject, updateTask, getTaskCountByStatus, getTodayMaxSequence, getAllCommitments, getCommitmentsByStatus, getCommitmentsByTaskId, getCommitment, updateCommitment, getRecentMessages, getMessageStats, getMessageLogById, getTraceSpans, listTraces, messageLog, auditEvents, commitments, orgMembers, orgOwnership, getOrgMember, getOpenOpportunities, getClarifications, getAllChats, getChat, getChatMembers, getMemberChats, getChatStats, getAllProjectsWithStats, getProjectWithRepos, upsertProject, syncProjectRepos, tasks, getCCSessionsByTask, getCCSessionBySessionId, getAllCCSessions, getCCSessionsByChat, } from '@team-anya/db';
|
|
2
|
+
import { readSessionConversation, readSessionStream } from './session-reader.js';
|
|
3
|
+
import { messageEvents } from './message-events.js';
|
|
4
|
+
import { eq, and, gte, lte, desc, sql } from 'drizzle-orm';
|
|
5
|
+
import { TaskStatus, CommitmentStatus, generateTaskId, validateTransition } from '@team-anya/core';
|
|
6
|
+
import * as os from 'node:os';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import { Clarifier } from '../loid/clarifier.js';
|
|
10
|
+
import { OpportunityManager } from '../loid/opportunity-manager.js';
|
|
11
|
+
import { SelfCalibrator } from '../loid/self-calibrator.js';
|
|
12
|
+
import { parseFeishuMessage, resolveFeishuUser } from './feishu-ws.js';
|
|
13
|
+
export async function registerRoutes(app, deps) {
|
|
14
|
+
const { db, broker } = deps;
|
|
15
|
+
const clarifier = deps.clarifier ?? new Clarifier({ db });
|
|
16
|
+
// POST /api/tasks — 创建任务
|
|
17
|
+
app.post('/api/tasks', async (request, reply) => {
|
|
18
|
+
const body = request.body;
|
|
19
|
+
if (!body || !body.title || !body.source_type) {
|
|
20
|
+
return reply.status(400).send({ error: '缺少必填字段: title, source_type' });
|
|
21
|
+
}
|
|
22
|
+
const validSourceTypes = ['file', 'feishu', 'meeting_note', 'manual'];
|
|
23
|
+
if (!validSourceTypes.includes(body.source_type)) {
|
|
24
|
+
return reply.status(400).send({ error: `无效的 source_type: ${body.source_type}` });
|
|
25
|
+
}
|
|
26
|
+
const seq = getTodayMaxSequence(db) + 1;
|
|
27
|
+
const taskId = generateTaskId(seq);
|
|
28
|
+
const task = createTask(db, {
|
|
29
|
+
task_id: taskId,
|
|
30
|
+
title: body.title,
|
|
31
|
+
status: TaskStatus.NEW,
|
|
32
|
+
source_type: body.source_type,
|
|
33
|
+
source_ref: body.source_ref ?? null,
|
|
34
|
+
objective: body.objective ?? null,
|
|
35
|
+
acceptance_criteria: body.acceptance_criteria
|
|
36
|
+
? JSON.stringify(body.acceptance_criteria)
|
|
37
|
+
: null,
|
|
38
|
+
context: body.context ?? null,
|
|
39
|
+
project_id: body.project_id ?? null,
|
|
40
|
+
created_by: body.created_by ?? null,
|
|
41
|
+
source_chat_id: body.source_chat_id ?? null,
|
|
42
|
+
});
|
|
43
|
+
return reply.status(201).send(task);
|
|
44
|
+
});
|
|
45
|
+
// GET /api/tasks — 列出任务(支持多条件组合过滤)
|
|
46
|
+
app.get('/api/tasks', async (request, reply) => {
|
|
47
|
+
const { status, project_id, assignee, source_chat_id, source_type } = request.query;
|
|
48
|
+
const conditions = [];
|
|
49
|
+
if (status)
|
|
50
|
+
conditions.push(eq(tasks.status, status));
|
|
51
|
+
if (project_id)
|
|
52
|
+
conditions.push(eq(tasks.project_id, project_id));
|
|
53
|
+
if (assignee)
|
|
54
|
+
conditions.push(eq(tasks.assignee, assignee));
|
|
55
|
+
if (source_chat_id)
|
|
56
|
+
conditions.push(eq(tasks.source_chat_id, source_chat_id));
|
|
57
|
+
if (source_type)
|
|
58
|
+
conditions.push(eq(tasks.source_type, source_type));
|
|
59
|
+
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
|
|
60
|
+
const result = db.select().from(tasks).where(whereClause).orderBy(desc(tasks.created_at)).all();
|
|
61
|
+
return reply.send(result);
|
|
62
|
+
});
|
|
63
|
+
// GET /api/tasks/:task_id — 任务详情
|
|
64
|
+
app.get('/api/tasks/:task_id', async (request, reply) => {
|
|
65
|
+
const task = getTask(db, request.params.task_id);
|
|
66
|
+
if (!task) {
|
|
67
|
+
return reply.status(404).send({ error: '任务不存在' });
|
|
68
|
+
}
|
|
69
|
+
return reply.send(task);
|
|
70
|
+
});
|
|
71
|
+
// PATCH /api/tasks/:task_id/status — 人类手动状态变更
|
|
72
|
+
app.patch('/api/tasks/:task_id/status', async (request, reply) => {
|
|
73
|
+
const { task_id } = request.params;
|
|
74
|
+
const body = request.body;
|
|
75
|
+
if (!body || !body.status) {
|
|
76
|
+
return reply.status(400).send({ error: '缺少必填字段: status' });
|
|
77
|
+
}
|
|
78
|
+
const task = getTask(db, task_id);
|
|
79
|
+
if (!task) {
|
|
80
|
+
return reply.status(404).send({ error: '任务不存在' });
|
|
81
|
+
}
|
|
82
|
+
const from = task.status;
|
|
83
|
+
const to = body.status;
|
|
84
|
+
if (!validateTransition(from, to)) {
|
|
85
|
+
return reply.status(400).send({
|
|
86
|
+
error: `非法状态转换: ${from} → ${to}`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const updateData = { status: to };
|
|
90
|
+
if (to === TaskStatus.BLOCKED && body.reason) {
|
|
91
|
+
updateData.blocked_reason = body.reason;
|
|
92
|
+
updateData.blocked_since = new Date().toISOString();
|
|
93
|
+
}
|
|
94
|
+
const updated = updateTask(db, task_id, updateData);
|
|
95
|
+
return reply.send(updated);
|
|
96
|
+
});
|
|
97
|
+
// GET /api/dashboard — 任务看板数据(C3 增强)
|
|
98
|
+
app.get('/api/dashboard', async (_request, reply) => {
|
|
99
|
+
const statusCounts = getTaskCountByStatus(db);
|
|
100
|
+
const allTasks = getAllTasks(db);
|
|
101
|
+
const slotStatus = broker.status();
|
|
102
|
+
// 逾期承诺数 (status = 'broken')
|
|
103
|
+
const brokenCommitments = db.select()
|
|
104
|
+
.from(commitments)
|
|
105
|
+
.where(eq(commitments.status, 'broken'))
|
|
106
|
+
.all();
|
|
107
|
+
const overdueCommitments = brokenCommitments.length;
|
|
108
|
+
// active yor instances 详情
|
|
109
|
+
const activeYorSlots = slotStatus
|
|
110
|
+
.filter(s => s.role === 'yor' && s.state !== 'disposed')
|
|
111
|
+
.map(s => ({
|
|
112
|
+
id: s.id,
|
|
113
|
+
taskId: s.taskId,
|
|
114
|
+
startedAt: s.createdAt,
|
|
115
|
+
}));
|
|
116
|
+
// 今日事件统计
|
|
117
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
118
|
+
const todayStart = `${today}T00:00:00Z`;
|
|
119
|
+
const todayEnd = `${today}T23:59:59Z`;
|
|
120
|
+
const todayEventsResult = db
|
|
121
|
+
.select({ count: sql `count(*)` })
|
|
122
|
+
.from(auditEvents)
|
|
123
|
+
.where(and(gte(auditEvents.created_at, todayStart), lte(auditEvents.created_at, todayEnd)))
|
|
124
|
+
.get();
|
|
125
|
+
const todayEventsCount = todayEventsResult?.count ?? 0;
|
|
126
|
+
// 未处理机会
|
|
127
|
+
const recentOpportunities = getOpenOpportunities(db);
|
|
128
|
+
return reply.send({
|
|
129
|
+
statusCounts,
|
|
130
|
+
totalTasks: allTasks.length,
|
|
131
|
+
activeTasks: allTasks.filter(t => t.status !== TaskStatus.DONE && t.status !== TaskStatus.CANCELLED).length,
|
|
132
|
+
yorSlots: {
|
|
133
|
+
total: slotStatus.filter(s => s.role === 'yor').length,
|
|
134
|
+
busy: slotStatus.filter(s => s.role === 'yor' && s.state === 'executing').length,
|
|
135
|
+
available: 0,
|
|
136
|
+
},
|
|
137
|
+
recentTasks: allTasks.slice(-10).reverse(),
|
|
138
|
+
// C3 新增字段
|
|
139
|
+
overdueCommitments,
|
|
140
|
+
activeYorSlots,
|
|
141
|
+
todayEventsCount,
|
|
142
|
+
recentOpportunities,
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
// ── Commitment API ──
|
|
146
|
+
// GET /api/commitments — 列出承诺(支持 status, task_id 过滤)
|
|
147
|
+
app.get('/api/commitments', async (request, reply) => {
|
|
148
|
+
const { status, task_id } = request.query;
|
|
149
|
+
let result;
|
|
150
|
+
if (status) {
|
|
151
|
+
result = getCommitmentsByStatus(db, status);
|
|
152
|
+
}
|
|
153
|
+
else if (task_id) {
|
|
154
|
+
result = getCommitmentsByTaskId(db, task_id);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
result = getAllCommitments(db);
|
|
158
|
+
}
|
|
159
|
+
return reply.send(result);
|
|
160
|
+
});
|
|
161
|
+
// PATCH /api/commitments/:id — 更新承诺状态
|
|
162
|
+
app.patch('/api/commitments/:id', async (request, reply) => {
|
|
163
|
+
const id = parseInt(request.params.id, 10);
|
|
164
|
+
const body = request.body;
|
|
165
|
+
if (!body || !body.status) {
|
|
166
|
+
return reply.status(400).send({ error: '缺少必填字段: status' });
|
|
167
|
+
}
|
|
168
|
+
const validStatuses = [
|
|
169
|
+
CommitmentStatus.FULFILLED,
|
|
170
|
+
CommitmentStatus.BROKEN,
|
|
171
|
+
CommitmentStatus.CANCELLED,
|
|
172
|
+
];
|
|
173
|
+
if (!validStatuses.includes(body.status)) {
|
|
174
|
+
return reply.status(400).send({
|
|
175
|
+
error: `无效的状态: ${body.status},合法值: ${validStatuses.join(', ')}`,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const existing = getCommitment(db, id);
|
|
179
|
+
if (!existing) {
|
|
180
|
+
return reply.status(404).send({ error: '承诺不存在' });
|
|
181
|
+
}
|
|
182
|
+
const updateData = {
|
|
183
|
+
status: body.status,
|
|
184
|
+
};
|
|
185
|
+
if (body.reason) {
|
|
186
|
+
updateData.break_reason = body.reason;
|
|
187
|
+
}
|
|
188
|
+
const updated = updateCommitment(db, id, updateData);
|
|
189
|
+
return reply.send(updated);
|
|
190
|
+
});
|
|
191
|
+
// ── Clarification API ──
|
|
192
|
+
// POST /api/tasks/:task_id/clarify — 触发需求澄清
|
|
193
|
+
app.post('/api/tasks/:task_id/clarify', async (request, reply) => {
|
|
194
|
+
const { task_id } = request.params;
|
|
195
|
+
const senderId = request.body?.sender_id;
|
|
196
|
+
try {
|
|
197
|
+
const result = await clarifier.clarifyTask(task_id, senderId);
|
|
198
|
+
return reply.send(result);
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
if (err.message?.includes('任务不存在')) {
|
|
202
|
+
return reply.status(404).send({ error: err.message });
|
|
203
|
+
}
|
|
204
|
+
if (err.message?.includes('只有 NEW 状态')) {
|
|
205
|
+
return reply.status(400).send({ error: err.message });
|
|
206
|
+
}
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
// POST /api/tasks/:task_id/clarify/:id/answer — 回答澄清问题
|
|
211
|
+
app.post('/api/tasks/:task_id/clarify/:id/answer', async (request, reply) => {
|
|
212
|
+
const clarificationId = parseInt(request.params.id, 10);
|
|
213
|
+
const body = request.body;
|
|
214
|
+
if (!body || !body.answer || !body.answered_by) {
|
|
215
|
+
return reply.status(400).send({ error: '缺少必填字段: answer, answered_by' });
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
await clarifier.handleAnswer(clarificationId, body.answer, body.answered_by);
|
|
219
|
+
return reply.send({ ok: true });
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
if (err.message?.includes('不存在')) {
|
|
223
|
+
return reply.status(404).send({ error: err.message });
|
|
224
|
+
}
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
// GET /api/tasks/:task_id/clarifications — 查询任务澄清记录
|
|
229
|
+
app.get('/api/tasks/:task_id/clarifications', async (request, reply) => {
|
|
230
|
+
const { task_id } = request.params;
|
|
231
|
+
const task = getTask(db, task_id);
|
|
232
|
+
if (!task) {
|
|
233
|
+
return reply.status(404).send({ error: '任务不存在' });
|
|
234
|
+
}
|
|
235
|
+
const clarifications = getClarifications(db, task_id);
|
|
236
|
+
return reply.send(clarifications);
|
|
237
|
+
});
|
|
238
|
+
// ── Audit API ──
|
|
239
|
+
// GET /api/audit — 审计事件查询
|
|
240
|
+
app.get('/api/audit', async (request, reply) => {
|
|
241
|
+
const { event_type, task_id, actor, from, to } = request.query;
|
|
242
|
+
const limit = Math.min(Math.max(parseInt(request.query.limit || '50', 10) || 50, 1), 500);
|
|
243
|
+
const offset = Math.max(parseInt(request.query.offset || '0', 10) || 0, 0);
|
|
244
|
+
// 构建过滤条件
|
|
245
|
+
const conditions = [];
|
|
246
|
+
if (event_type) {
|
|
247
|
+
conditions.push(eq(auditEvents.event_type, event_type));
|
|
248
|
+
}
|
|
249
|
+
if (task_id) {
|
|
250
|
+
conditions.push(eq(auditEvents.task_id, task_id));
|
|
251
|
+
}
|
|
252
|
+
if (actor) {
|
|
253
|
+
conditions.push(eq(auditEvents.actor, actor));
|
|
254
|
+
}
|
|
255
|
+
if (from) {
|
|
256
|
+
conditions.push(gte(auditEvents.created_at, from));
|
|
257
|
+
}
|
|
258
|
+
if (to) {
|
|
259
|
+
conditions.push(lte(auditEvents.created_at, to));
|
|
260
|
+
}
|
|
261
|
+
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
|
|
262
|
+
// 查询总数
|
|
263
|
+
const countResult = db
|
|
264
|
+
.select({ count: sql `count(*)` })
|
|
265
|
+
.from(auditEvents)
|
|
266
|
+
.where(whereClause)
|
|
267
|
+
.get();
|
|
268
|
+
const total = countResult?.count ?? 0;
|
|
269
|
+
// 查询数据(分页 + 排序)
|
|
270
|
+
const rows = db
|
|
271
|
+
.select()
|
|
272
|
+
.from(auditEvents)
|
|
273
|
+
.where(whereClause)
|
|
274
|
+
.orderBy(desc(auditEvents.created_at))
|
|
275
|
+
.limit(limit)
|
|
276
|
+
.offset(offset)
|
|
277
|
+
.all();
|
|
278
|
+
// 解析 detail JSON
|
|
279
|
+
const data = rows.map(row => ({
|
|
280
|
+
...row,
|
|
281
|
+
detail: parseDetail(row.detail),
|
|
282
|
+
}));
|
|
283
|
+
return reply.send({
|
|
284
|
+
data,
|
|
285
|
+
total,
|
|
286
|
+
limit,
|
|
287
|
+
offset,
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
// ── C3: 新增 API ──
|
|
291
|
+
// GET /api/reports/daily/:date — 获取日报
|
|
292
|
+
app.get('/api/reports/daily/:date', async (request, reply) => {
|
|
293
|
+
const { date } = request.params;
|
|
294
|
+
const dayStart = `${date}T00:00:00Z`;
|
|
295
|
+
const dayEnd = `${date}T23:59:59Z`;
|
|
296
|
+
// 今日事件数
|
|
297
|
+
const eventsResult = db
|
|
298
|
+
.select({ count: sql `count(*)` })
|
|
299
|
+
.from(auditEvents)
|
|
300
|
+
.where(and(gte(auditEvents.created_at, dayStart), lte(auditEvents.created_at, dayEnd)))
|
|
301
|
+
.get();
|
|
302
|
+
const eventsCount = eventsResult?.count ?? 0;
|
|
303
|
+
// 任务状态汇总
|
|
304
|
+
const taskSummary = getTaskCountByStatus(db);
|
|
305
|
+
// 承诺状态汇总
|
|
306
|
+
const allCommitments = getAllCommitments(db);
|
|
307
|
+
const commitmentSummary = {
|
|
308
|
+
active: allCommitments.filter(c => c.status === 'active').length,
|
|
309
|
+
fulfilled: allCommitments.filter(c => c.status === 'fulfilled').length,
|
|
310
|
+
broken: allCommitments.filter(c => c.status === 'broken').length,
|
|
311
|
+
};
|
|
312
|
+
// 未处理机会
|
|
313
|
+
const openOpportunities = getOpenOpportunities(db);
|
|
314
|
+
return reply.send({
|
|
315
|
+
date,
|
|
316
|
+
eventsCount,
|
|
317
|
+
taskSummary,
|
|
318
|
+
commitmentSummary,
|
|
319
|
+
openOpportunities: openOpportunities.length,
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
// GET /api/org/members — 团队成员列表
|
|
323
|
+
app.get('/api/org/members', async (_request, reply) => {
|
|
324
|
+
const members = db.select().from(orgMembers).all();
|
|
325
|
+
return reply.send(members);
|
|
326
|
+
});
|
|
327
|
+
// GET /api/org/members/:member_id — 成员详情 + ownership + 所在群列表
|
|
328
|
+
app.get('/api/org/members/:member_id', async (request, reply) => {
|
|
329
|
+
const { member_id } = request.params;
|
|
330
|
+
const member = getOrgMember(db, member_id);
|
|
331
|
+
if (!member) {
|
|
332
|
+
return reply.status(404).send({ error: '成员不存在' });
|
|
333
|
+
}
|
|
334
|
+
const ownership = db.select()
|
|
335
|
+
.from(orgOwnership)
|
|
336
|
+
.where(eq(orgOwnership.member_id, member_id))
|
|
337
|
+
.all();
|
|
338
|
+
const memberChats = getMemberChats(db, member_id);
|
|
339
|
+
return reply.send({
|
|
340
|
+
member,
|
|
341
|
+
ownership,
|
|
342
|
+
chats: memberChats,
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
// ── Projects API ──
|
|
346
|
+
// GET /api/projects — 项目列表(含任务统计)
|
|
347
|
+
app.get('/api/projects', async (_request, reply) => {
|
|
348
|
+
const projects = getAllProjectsWithStats(db);
|
|
349
|
+
return reply.send({ data: projects, total: projects.length, success: true });
|
|
350
|
+
});
|
|
351
|
+
// GET /api/projects/:projectId — 项目详情
|
|
352
|
+
app.get('/api/projects/:projectId', async (request, reply) => {
|
|
353
|
+
const { projectId } = request.params;
|
|
354
|
+
const projectWithRepos = getProjectWithRepos(db, projectId);
|
|
355
|
+
if (!projectWithRepos) {
|
|
356
|
+
return reply.status(404).send({ error: '项目不存在' });
|
|
357
|
+
}
|
|
358
|
+
const projectTasks = getTasksByProject(db, projectId);
|
|
359
|
+
const statusCounts = {};
|
|
360
|
+
for (const t of projectTasks) {
|
|
361
|
+
statusCounts[t.status] = (statusCounts[t.status] || 0) + 1;
|
|
362
|
+
}
|
|
363
|
+
return reply.send({
|
|
364
|
+
project: projectWithRepos,
|
|
365
|
+
tasks: projectTasks,
|
|
366
|
+
statusCounts,
|
|
367
|
+
taskCount: projectTasks.length,
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
// POST /api/projects — 创建/更新项目
|
|
371
|
+
app.post('/api/projects', async (request, reply) => {
|
|
372
|
+
const body = request.body;
|
|
373
|
+
if (!body || !body.project_id || !body.name) {
|
|
374
|
+
return reply.status(400).send({ error: '缺少必填字段: project_id, name' });
|
|
375
|
+
}
|
|
376
|
+
const project = upsertProject(db, {
|
|
377
|
+
project_id: body.project_id,
|
|
378
|
+
name: body.name,
|
|
379
|
+
description: body.description ?? null,
|
|
380
|
+
platform: body.platform ?? 'github',
|
|
381
|
+
claude_md: body.claude_md ?? null,
|
|
382
|
+
});
|
|
383
|
+
if (body.repos) {
|
|
384
|
+
syncProjectRepos(db, body.project_id, body.repos);
|
|
385
|
+
}
|
|
386
|
+
return reply.status(201).send(project);
|
|
387
|
+
});
|
|
388
|
+
// ── Chats API ──
|
|
389
|
+
// GET /api/chats — 群聊列表(支持 platform/chat_type/name 过滤)
|
|
390
|
+
app.get('/api/chats', async (request, reply) => {
|
|
391
|
+
const { platform, chat_type, name } = request.query;
|
|
392
|
+
const result = getAllChats(db, { platform, chat_type, name });
|
|
393
|
+
return reply.send({ data: result, total: result.length, success: true });
|
|
394
|
+
});
|
|
395
|
+
// GET /api/chats/:chat_id — 群详情
|
|
396
|
+
app.get('/api/chats/:chat_id', async (request, reply) => {
|
|
397
|
+
const chat = getChat(db, request.params.chat_id);
|
|
398
|
+
if (!chat) {
|
|
399
|
+
return reply.status(404).send({ error: '群聊不存在' });
|
|
400
|
+
}
|
|
401
|
+
return reply.send(chat);
|
|
402
|
+
});
|
|
403
|
+
// GET /api/chats/:chat_id/members — 群成员列表
|
|
404
|
+
app.get('/api/chats/:chat_id/members', async (request, reply) => {
|
|
405
|
+
const chat = getChat(db, request.params.chat_id);
|
|
406
|
+
if (!chat) {
|
|
407
|
+
return reply.status(404).send({ error: '群聊不存在' });
|
|
408
|
+
}
|
|
409
|
+
const members = getChatMembers(db, request.params.chat_id);
|
|
410
|
+
return reply.send(members);
|
|
411
|
+
});
|
|
412
|
+
// GET /api/chats/:chat_id/messages — 某群的消息历史
|
|
413
|
+
app.get('/api/chats/:chat_id/messages', async (request, reply) => {
|
|
414
|
+
const limit = Math.min(Math.max(parseInt(request.query.limit || '20', 10) || 20, 1), 500);
|
|
415
|
+
const offset = Math.max(parseInt(request.query.offset || '0', 10) || 0, 0);
|
|
416
|
+
const messages = getRecentMessages(db, {
|
|
417
|
+
chat_id: request.params.chat_id,
|
|
418
|
+
limit,
|
|
419
|
+
offset,
|
|
420
|
+
});
|
|
421
|
+
return reply.send({ data: messages, success: true });
|
|
422
|
+
});
|
|
423
|
+
// POST /api/chats/:chat_id/sync — 手动触发同步群信息+成员
|
|
424
|
+
app.post('/api/chats/:chat_id/sync', async (request, reply) => {
|
|
425
|
+
if (!deps.chatSyncService) {
|
|
426
|
+
return reply.status(503).send({ error: '飞书同步服务未配置(缺少飞书凭证)' });
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
await deps.chatSyncService.syncChatFull(request.params.chat_id);
|
|
430
|
+
const chat = getChat(db, request.params.chat_id);
|
|
431
|
+
return reply.send({ ok: true, chat });
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
return reply.status(500).send({ error: `同步失败: ${err.message}` });
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
// GET /api/chats/stats — 群统计
|
|
438
|
+
app.get('/api/chats/stats', async (_request, reply) => {
|
|
439
|
+
const stats = getChatStats(db);
|
|
440
|
+
return reply.send(stats);
|
|
441
|
+
});
|
|
442
|
+
// GET /api/org/ownership — 归属关系查询(支持 target, member_id 过滤)
|
|
443
|
+
app.get('/api/org/ownership', async (request, reply) => {
|
|
444
|
+
const { target, member_id } = request.query;
|
|
445
|
+
const conditions = [];
|
|
446
|
+
if (target) {
|
|
447
|
+
conditions.push(eq(orgOwnership.target, target));
|
|
448
|
+
}
|
|
449
|
+
if (member_id) {
|
|
450
|
+
conditions.push(eq(orgOwnership.member_id, member_id));
|
|
451
|
+
}
|
|
452
|
+
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
|
|
453
|
+
const result = db.select()
|
|
454
|
+
.from(orgOwnership)
|
|
455
|
+
.where(whereClause)
|
|
456
|
+
.all();
|
|
457
|
+
return reply.send(result);
|
|
458
|
+
});
|
|
459
|
+
// ── B3: Opportunity API ──
|
|
460
|
+
const opportunityManager = new OpportunityManager({
|
|
461
|
+
db,
|
|
462
|
+
workspacePath: '/tmp/anya', // TODO: 从 config 获取
|
|
463
|
+
});
|
|
464
|
+
// GET /api/opportunities — 查询机会列表
|
|
465
|
+
app.get('/api/opportunities', async (request, reply) => {
|
|
466
|
+
const { status, min_score } = request.query;
|
|
467
|
+
const filter = {};
|
|
468
|
+
if (status)
|
|
469
|
+
filter.status = status;
|
|
470
|
+
if (min_score)
|
|
471
|
+
filter.min_score = parseFloat(min_score);
|
|
472
|
+
const result = opportunityManager.getOpportunities(filter);
|
|
473
|
+
return reply.send(result);
|
|
474
|
+
});
|
|
475
|
+
// PATCH /api/opportunities/:id — confirm 或 reject 机会
|
|
476
|
+
app.patch('/api/opportunities/:id', async (request, reply) => {
|
|
477
|
+
const { id } = request.params;
|
|
478
|
+
const body = request.body;
|
|
479
|
+
if (!body || !body.action) {
|
|
480
|
+
return reply.status(400).send({ error: '缺少必填字段: action' });
|
|
481
|
+
}
|
|
482
|
+
if (body.action !== 'confirm' && body.action !== 'reject') {
|
|
483
|
+
return reply.status(400).send({ error: `无效的 action: ${body.action},合法值: confirm, reject` });
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
if (body.action === 'confirm') {
|
|
487
|
+
const result = opportunityManager.confirm(id);
|
|
488
|
+
return reply.send(result);
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
const result = opportunityManager.reject(id, body.reason);
|
|
492
|
+
return reply.send(result);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
catch (err) {
|
|
496
|
+
if (err.message?.includes('不存在')) {
|
|
497
|
+
return reply.status(404).send({ error: err.message });
|
|
498
|
+
}
|
|
499
|
+
if (err.message?.includes('只有 detected 状态')) {
|
|
500
|
+
return reply.status(400).send({ error: err.message });
|
|
501
|
+
}
|
|
502
|
+
throw err;
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
// ── C5: Calibration API ──
|
|
506
|
+
const selfCalibrator = new SelfCalibrator({
|
|
507
|
+
db,
|
|
508
|
+
workspacePath: '/tmp/anya', // TODO: 从 config 获取
|
|
509
|
+
});
|
|
510
|
+
// GET /api/calibration/status — 获取当前校准状态
|
|
511
|
+
app.get('/api/calibration/status', async (_request, reply) => {
|
|
512
|
+
const status = selfCalibrator.getCalibrationStatus();
|
|
513
|
+
return reply.send(status);
|
|
514
|
+
});
|
|
515
|
+
// POST /api/calibration/run — 手动触发校准
|
|
516
|
+
app.post('/api/calibration/run', async (_request, reply) => {
|
|
517
|
+
const status = selfCalibrator.runCalibration();
|
|
518
|
+
return reply.send(status);
|
|
519
|
+
});
|
|
520
|
+
// ── Message Log API ──
|
|
521
|
+
// GET /api/messages — 分页查询消息
|
|
522
|
+
app.get('/api/messages', async (request, reply) => {
|
|
523
|
+
const { direction, since, message_type, sender, receiver, chat_id, chat_type, start_date, sort: sortOrder } = request.query;
|
|
524
|
+
const page = Math.max(parseInt(request.query.page || '1', 10) || 1, 1);
|
|
525
|
+
const pageSize = Math.min(Math.max(parseInt(request.query.pageSize || request.query.limit || '20', 10) || 20, 1), 500);
|
|
526
|
+
const offset = request.query.offset ? Math.max(parseInt(request.query.offset, 10) || 0, 0) : (page - 1) * pageSize;
|
|
527
|
+
const messages = getRecentMessages(db, {
|
|
528
|
+
limit: pageSize,
|
|
529
|
+
offset,
|
|
530
|
+
direction,
|
|
531
|
+
since: since ?? start_date,
|
|
532
|
+
message_type,
|
|
533
|
+
sender,
|
|
534
|
+
receiver,
|
|
535
|
+
chat_id,
|
|
536
|
+
chat_type,
|
|
537
|
+
sort: sortOrder,
|
|
538
|
+
});
|
|
539
|
+
// 获取总数(用于分页)
|
|
540
|
+
const conditions = [];
|
|
541
|
+
if (direction)
|
|
542
|
+
conditions.push(eq(messageLog.direction, direction));
|
|
543
|
+
if (since ?? start_date)
|
|
544
|
+
conditions.push(gte(messageLog.created_at, (since ?? start_date)));
|
|
545
|
+
if (message_type)
|
|
546
|
+
conditions.push(eq(messageLog.message_type, message_type));
|
|
547
|
+
if (sender)
|
|
548
|
+
conditions.push(eq(messageLog.sender, sender));
|
|
549
|
+
if (receiver)
|
|
550
|
+
conditions.push(eq(messageLog.receiver, receiver));
|
|
551
|
+
if (chat_id)
|
|
552
|
+
conditions.push(eq(messageLog.chat_id, chat_id));
|
|
553
|
+
if (chat_type)
|
|
554
|
+
conditions.push(eq(messageLog.chat_type, chat_type));
|
|
555
|
+
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
|
|
556
|
+
const countResult = db
|
|
557
|
+
.select({ count: sql `count(*)` })
|
|
558
|
+
.from(messageLog)
|
|
559
|
+
.where(whereClause)
|
|
560
|
+
.get();
|
|
561
|
+
return reply.send({
|
|
562
|
+
data: messages,
|
|
563
|
+
total: countResult?.count ?? 0,
|
|
564
|
+
success: true,
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
// GET /api/messages/stats — 今日消息统计
|
|
568
|
+
app.get('/api/messages/stats', async (_request, reply) => {
|
|
569
|
+
const stats = getMessageStats(db);
|
|
570
|
+
return reply.send(stats);
|
|
571
|
+
});
|
|
572
|
+
// ── Trace API ──
|
|
573
|
+
// GET /api/traces — 查询链路列表
|
|
574
|
+
app.get('/api/traces', async (request, reply) => {
|
|
575
|
+
const limit = Math.min(Math.max(parseInt(request.query.limit || '50', 10) || 50, 1), 200);
|
|
576
|
+
const offset = Math.max(parseInt(request.query.offset || '0', 10) || 0, 0);
|
|
577
|
+
const { operation, status } = request.query;
|
|
578
|
+
const result = listTraces(db, { limit, offset, operation, status });
|
|
579
|
+
return reply.send({
|
|
580
|
+
data: result.data,
|
|
581
|
+
total: result.total,
|
|
582
|
+
success: true,
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
// GET /api/traces/:traceId — 查询链路详情
|
|
586
|
+
app.get('/api/traces/:traceId', async (request, reply) => {
|
|
587
|
+
const { traceId } = request.params;
|
|
588
|
+
const spans = getTraceSpans(db, traceId);
|
|
589
|
+
if (spans.length === 0) {
|
|
590
|
+
return reply.status(404).send({ error: '链路不存在' });
|
|
591
|
+
}
|
|
592
|
+
// 计算总耗时
|
|
593
|
+
const startTimes = spans.map(s => new Date(s.started_at).getTime());
|
|
594
|
+
const endTimes = spans
|
|
595
|
+
.filter(s => s.ended_at)
|
|
596
|
+
.map(s => new Date(s.ended_at).getTime());
|
|
597
|
+
const traceStart = Math.min(...startTimes);
|
|
598
|
+
const traceEnd = endTimes.length > 0 ? Math.max(...endTimes) : Date.now();
|
|
599
|
+
const totalDurationMs = traceEnd - traceStart;
|
|
600
|
+
// 整体状态
|
|
601
|
+
const hasError = spans.some(s => s.status === 'error');
|
|
602
|
+
const allDone = spans.every(s => s.status !== 'in_progress');
|
|
603
|
+
const traceStatus = hasError ? 'error' : allDone ? 'success' : 'in_progress';
|
|
604
|
+
// 解析 JSON 字段
|
|
605
|
+
const parsedSpans = spans.map(s => ({
|
|
606
|
+
...s,
|
|
607
|
+
input: safeJsonParse(s.input),
|
|
608
|
+
output: safeJsonParse(s.output),
|
|
609
|
+
metadata: safeJsonParse(s.metadata),
|
|
610
|
+
}));
|
|
611
|
+
return reply.send({
|
|
612
|
+
traceId,
|
|
613
|
+
status: traceStatus,
|
|
614
|
+
totalDurationMs,
|
|
615
|
+
spanCount: spans.length,
|
|
616
|
+
spans: parsedSpans,
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
// GET /api/messages/:id/trace — 从消息跳转到链路
|
|
620
|
+
app.get('/api/messages/:id/trace', async (request, reply) => {
|
|
621
|
+
const id = parseInt(request.params.id, 10);
|
|
622
|
+
const msg = getMessageLogById(db, id);
|
|
623
|
+
if (!msg) {
|
|
624
|
+
return reply.status(404).send({ error: '消息不存在' });
|
|
625
|
+
}
|
|
626
|
+
if (!msg.trace_id) {
|
|
627
|
+
return reply.status(404).send({ error: '该消息无链路信息' });
|
|
628
|
+
}
|
|
629
|
+
return reply.redirect(`/api/traces/${msg.trace_id}`);
|
|
630
|
+
});
|
|
631
|
+
// ── CC Session API ──
|
|
632
|
+
// GET /api/tasks/:task_id/sessions — 查询任务关联的 CC session 列表
|
|
633
|
+
app.get('/api/tasks/:task_id/sessions', async (request, reply) => {
|
|
634
|
+
const { task_id } = request.params;
|
|
635
|
+
const task = getTask(db, task_id);
|
|
636
|
+
if (!task) {
|
|
637
|
+
return reply.status(404).send({ error: '任务不存在' });
|
|
638
|
+
}
|
|
639
|
+
const sessions = getCCSessionsByTask(db, task_id);
|
|
640
|
+
return reply.send({ data: sessions, success: true });
|
|
641
|
+
});
|
|
642
|
+
// GET /api/sessions/:sessionId — 读取 CC 会话完整对话
|
|
643
|
+
app.get('/api/sessions/:sessionId', async (request, reply) => {
|
|
644
|
+
const { sessionId } = request.params;
|
|
645
|
+
// 先查 DB 获取 project_path
|
|
646
|
+
const ccSession = getCCSessionBySessionId(db, sessionId);
|
|
647
|
+
const projectPath = ccSession?.project_path ?? undefined;
|
|
648
|
+
const messages = readSessionConversation(sessionId, projectPath);
|
|
649
|
+
if (messages.length === 0) {
|
|
650
|
+
return reply.status(404).send({ error: '会话文件不存在或为空' });
|
|
651
|
+
}
|
|
652
|
+
return reply.send({ data: messages, success: true });
|
|
653
|
+
});
|
|
654
|
+
// GET /api/sessions/:sessionId/info — 获取 session 元信息 + 关联 task
|
|
655
|
+
app.get('/api/sessions/:sessionId/info', async (request, reply) => {
|
|
656
|
+
const { sessionId } = request.params;
|
|
657
|
+
const ccSession = getCCSessionBySessionId(db, sessionId);
|
|
658
|
+
let task = null;
|
|
659
|
+
if (ccSession?.task_id) {
|
|
660
|
+
task = getTask(db, ccSession.task_id);
|
|
661
|
+
}
|
|
662
|
+
return reply.send({
|
|
663
|
+
success: true,
|
|
664
|
+
data: {
|
|
665
|
+
session: ccSession ?? null,
|
|
666
|
+
task: task ? {
|
|
667
|
+
...task,
|
|
668
|
+
acceptance_criteria: safeJsonParse(task.acceptance_criteria),
|
|
669
|
+
} : null,
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
// GET /api/sessions/:sessionId/stream — SSE 流式推送会话消息
|
|
674
|
+
app.get('/api/sessions/:sessionId/stream', async (request, reply) => {
|
|
675
|
+
const { sessionId } = request.params;
|
|
676
|
+
const offsetParam = request.query.offset;
|
|
677
|
+
let offset = offsetParam ? parseInt(offsetParam, 10) : 0;
|
|
678
|
+
const ccSession = getCCSessionBySessionId(db, sessionId);
|
|
679
|
+
const projectPath = ccSession?.project_path ?? undefined;
|
|
680
|
+
reply.raw.writeHead(200, {
|
|
681
|
+
'Content-Type': 'text/event-stream',
|
|
682
|
+
'Cache-Control': 'no-cache',
|
|
683
|
+
'Connection': 'keep-alive',
|
|
684
|
+
});
|
|
685
|
+
// 发送初始数据
|
|
686
|
+
const initial = readSessionStream(sessionId, offset, projectPath);
|
|
687
|
+
offset = initial.nextOffset;
|
|
688
|
+
reply.raw.write(`event: messages\ndata: ${JSON.stringify(initial.messages)}\n\n`);
|
|
689
|
+
let isConnected = true;
|
|
690
|
+
request.raw.on('close', () => { isConnected = false; });
|
|
691
|
+
// 轮询文件变化(每 2 秒)
|
|
692
|
+
const pollInterval = setInterval(() => {
|
|
693
|
+
if (!isConnected) {
|
|
694
|
+
clearInterval(pollInterval);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
const result = readSessionStream(sessionId, offset, projectPath);
|
|
699
|
+
if (result.messages.length > 0) {
|
|
700
|
+
offset = result.nextOffset;
|
|
701
|
+
reply.raw.write(`event: messages\ndata: ${JSON.stringify(result.messages)}\n\n`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
// ignore read errors
|
|
706
|
+
}
|
|
707
|
+
}, 2000);
|
|
708
|
+
// 心跳(每 30 秒)
|
|
709
|
+
const heartbeatInterval = setInterval(() => {
|
|
710
|
+
if (!isConnected) {
|
|
711
|
+
clearInterval(heartbeatInterval);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
reply.raw.write(`event: heartbeat\ndata: ${JSON.stringify({ ts: Date.now() })}\n\n`);
|
|
715
|
+
}, 30000);
|
|
716
|
+
request.raw.on('close', () => {
|
|
717
|
+
clearInterval(pollInterval);
|
|
718
|
+
clearInterval(heartbeatInterval);
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
// GET /api/sessions — 全局会话列表(带筛选分页)
|
|
722
|
+
app.get('/api/sessions', async (request, reply) => {
|
|
723
|
+
const { role, task_id, chat_id, active, limit, offset } = request.query;
|
|
724
|
+
const result = getAllCCSessions(db, {
|
|
725
|
+
role: role || undefined,
|
|
726
|
+
taskId: task_id || undefined,
|
|
727
|
+
chatId: chat_id || undefined,
|
|
728
|
+
active: active === 'true' ? true : active === 'false' ? false : undefined,
|
|
729
|
+
limit: limit ? parseInt(limit, 10) : 50,
|
|
730
|
+
offset: offset ? parseInt(offset, 10) : 0,
|
|
731
|
+
});
|
|
732
|
+
return reply.send({ data: result.data, total: result.total, success: true });
|
|
733
|
+
});
|
|
734
|
+
// GET /api/chats/:chatId/sessions — 指定群聊的会话列表
|
|
735
|
+
app.get('/api/chats/:chatId/sessions', async (request, reply) => {
|
|
736
|
+
const { chatId } = request.params;
|
|
737
|
+
const chat = getChat(db, chatId);
|
|
738
|
+
if (!chat) {
|
|
739
|
+
return reply.status(404).send({ error: '群聊不存在' });
|
|
740
|
+
}
|
|
741
|
+
const sessions = getCCSessionsByChat(db, chatId);
|
|
742
|
+
return reply.send({ data: sessions, success: true });
|
|
743
|
+
});
|
|
744
|
+
// ── Settings API ──
|
|
745
|
+
const claudeSettingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
746
|
+
const claudeJsonPath = path.join(os.homedir(), '.claude.json');
|
|
747
|
+
// GET /api/settings/claude — 读取 Claude Code API 配置(API Key 脱敏)+ onboarding 状态
|
|
748
|
+
app.get('/api/settings/claude', async (_request, reply) => {
|
|
749
|
+
let settings = {};
|
|
750
|
+
try {
|
|
751
|
+
const raw = fs.readFileSync(claudeSettingsPath, 'utf-8');
|
|
752
|
+
settings = JSON.parse(raw);
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
// 文件不存在或解析失败,返回空配置
|
|
756
|
+
}
|
|
757
|
+
const env = settings.env ?? {};
|
|
758
|
+
const baseUrl = env.ANTHROPIC_BASE_URL ?? '';
|
|
759
|
+
const apiKey = env.ANTHROPIC_API_KEY ?? '';
|
|
760
|
+
// 脱敏:只显示前 8 位和后 4 位
|
|
761
|
+
let maskedKey = '';
|
|
762
|
+
if (apiKey.length > 12) {
|
|
763
|
+
maskedKey = apiKey.slice(0, 8) + '****' + apiKey.slice(-4);
|
|
764
|
+
}
|
|
765
|
+
else if (apiKey) {
|
|
766
|
+
maskedKey = '****';
|
|
767
|
+
}
|
|
768
|
+
// 读取 ~/.claude.json 中的 hasCompletedOnboarding
|
|
769
|
+
let skipOnboarding = false;
|
|
770
|
+
try {
|
|
771
|
+
const raw = fs.readFileSync(claudeJsonPath, 'utf-8');
|
|
772
|
+
const claudeJson = JSON.parse(raw);
|
|
773
|
+
skipOnboarding = claudeJson.hasCompletedOnboarding === true;
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
// 文件不存在
|
|
777
|
+
}
|
|
778
|
+
// providerMode: 如果 settings 中存储了 _providerMode 就用,否则根据 env 是否为空推断
|
|
779
|
+
const storedMode = settings._providerMode;
|
|
780
|
+
const providerMode = storedMode === 'official' ? 'official'
|
|
781
|
+
: storedMode === 'custom' ? 'custom'
|
|
782
|
+
: Object.keys(env).length > 0 ? 'custom' : 'official';
|
|
783
|
+
return reply.send({
|
|
784
|
+
providerMode,
|
|
785
|
+
baseUrl,
|
|
786
|
+
apiKey: maskedKey,
|
|
787
|
+
model: settings.model ?? '',
|
|
788
|
+
anthropicModel: env.ANTHROPIC_MODEL ?? '',
|
|
789
|
+
opusModel: env.ANTHROPIC_DEFAULT_OPUS_MODEL ?? '',
|
|
790
|
+
sonnetModel: env.ANTHROPIC_DEFAULT_SONNET_MODEL ?? '',
|
|
791
|
+
haikuModel: env.ANTHROPIC_DEFAULT_HAIKU_MODEL ?? '',
|
|
792
|
+
subagentModel: env.CLAUDE_CODE_SUBAGENT_MODEL ?? '',
|
|
793
|
+
skipOnboarding,
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
// POST /api/settings/claude — 写入 Claude Code API 配置 + 模型 + onboarding 开关
|
|
797
|
+
app.post('/api/settings/claude', async (request, reply) => {
|
|
798
|
+
const body = request.body;
|
|
799
|
+
if (!body) {
|
|
800
|
+
return reply.status(400).send({ error: '请求体为空' });
|
|
801
|
+
}
|
|
802
|
+
// ── 写入 ~/.claude/settings.json(env 字段)──
|
|
803
|
+
let settings = {};
|
|
804
|
+
try {
|
|
805
|
+
const raw = fs.readFileSync(claudeSettingsPath, 'utf-8');
|
|
806
|
+
settings = JSON.parse(raw);
|
|
807
|
+
}
|
|
808
|
+
catch {
|
|
809
|
+
// 文件不存在,从空对象开始
|
|
810
|
+
}
|
|
811
|
+
// 存储 providerMode
|
|
812
|
+
if (body.providerMode) {
|
|
813
|
+
settings._providerMode = body.providerMode;
|
|
814
|
+
}
|
|
815
|
+
// 官方登录模式:清除所有环境变量和模型配置
|
|
816
|
+
if (body.providerMode === 'official') {
|
|
817
|
+
delete settings.env;
|
|
818
|
+
delete settings.model;
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
const env = settings.env ?? {};
|
|
822
|
+
if (body.baseUrl !== undefined) {
|
|
823
|
+
if (body.baseUrl) {
|
|
824
|
+
env.ANTHROPIC_BASE_URL = body.baseUrl;
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
delete env.ANTHROPIC_BASE_URL;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (body.apiKey !== undefined && !body.apiKey.includes('****')) {
|
|
831
|
+
if (body.apiKey) {
|
|
832
|
+
env.ANTHROPIC_API_KEY = body.apiKey;
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
delete env.ANTHROPIC_API_KEY;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// 模型环境变量
|
|
839
|
+
const modelEnvMap = {
|
|
840
|
+
anthropicModel: 'ANTHROPIC_MODEL',
|
|
841
|
+
opusModel: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
842
|
+
sonnetModel: 'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
843
|
+
haikuModel: 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
844
|
+
subagentModel: 'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
845
|
+
};
|
|
846
|
+
for (const [field, envKey] of Object.entries(modelEnvMap)) {
|
|
847
|
+
const val = body[field];
|
|
848
|
+
if (val !== undefined) {
|
|
849
|
+
if (val) {
|
|
850
|
+
env[envKey] = String(val);
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
delete env[envKey];
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
settings.env = env;
|
|
858
|
+
}
|
|
859
|
+
// settings.json 顶层 model 字段(支持别名如 opus、sonnet、opusplan)
|
|
860
|
+
if (body.model !== undefined) {
|
|
861
|
+
if (body.model) {
|
|
862
|
+
settings.model = body.model;
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
delete settings.model;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
const dir = path.dirname(claudeSettingsPath);
|
|
869
|
+
if (!fs.existsSync(dir)) {
|
|
870
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
871
|
+
}
|
|
872
|
+
fs.writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
873
|
+
// ── 写入 ~/.claude.json(hasCompletedOnboarding)──
|
|
874
|
+
if (body.skipOnboarding !== undefined) {
|
|
875
|
+
let claudeJson = {};
|
|
876
|
+
try {
|
|
877
|
+
const raw = fs.readFileSync(claudeJsonPath, 'utf-8');
|
|
878
|
+
claudeJson = JSON.parse(raw);
|
|
879
|
+
}
|
|
880
|
+
catch {
|
|
881
|
+
// 文件不存在
|
|
882
|
+
}
|
|
883
|
+
if (body.skipOnboarding) {
|
|
884
|
+
claudeJson.hasCompletedOnboarding = true;
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
delete claudeJson.hasCompletedOnboarding;
|
|
888
|
+
}
|
|
889
|
+
fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2), 'utf-8');
|
|
890
|
+
}
|
|
891
|
+
return reply.send({ ok: true });
|
|
892
|
+
});
|
|
893
|
+
// ── Admin: 实例管理 ──
|
|
894
|
+
// POST /api/admin/restart — 重启所有 CC 实例
|
|
895
|
+
app.post('/api/admin/restart', async (_request, reply) => {
|
|
896
|
+
if (!deps.onRestart) {
|
|
897
|
+
return reply.status(503).send({ error: '重启功能未配置' });
|
|
898
|
+
}
|
|
899
|
+
const before = broker.status();
|
|
900
|
+
const activeCount = before.filter(i => i.state !== 'disposed').length;
|
|
901
|
+
if (activeCount === 0) {
|
|
902
|
+
return reply.send({ ok: true, disposed: 0, message: '当前没有活跃的 CC 实例' });
|
|
903
|
+
}
|
|
904
|
+
const result = await deps.onRestart();
|
|
905
|
+
return reply.send({
|
|
906
|
+
ok: true,
|
|
907
|
+
disposed: result.disposed,
|
|
908
|
+
message: `已释放 ${result.disposed} 个 CC 实例,会话将在下次交互时自动重建`,
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
// GET /api/admin/instances — 查看所有 CC 实例状态
|
|
912
|
+
app.get('/api/admin/instances', async (_request, reply) => {
|
|
913
|
+
const instances = broker.status();
|
|
914
|
+
return reply.send({
|
|
915
|
+
data: instances,
|
|
916
|
+
total: instances.length,
|
|
917
|
+
success: true,
|
|
918
|
+
});
|
|
919
|
+
});
|
|
920
|
+
// ── E2E 测试注入端点 ──
|
|
921
|
+
// POST /api/test/inject — 注入模拟飞书消息,走完整解析 + 用户名查询 + 处理流程
|
|
922
|
+
if (deps.messageQueue) {
|
|
923
|
+
const messageQueue = deps.messageQueue;
|
|
924
|
+
app.post('/api/test/inject', async (request, reply) => {
|
|
925
|
+
const body = request.body;
|
|
926
|
+
if (!body?.event?.message) {
|
|
927
|
+
return reply.status(400).send({ error: '缺少 event.message 字段' });
|
|
928
|
+
}
|
|
929
|
+
// 1. 解析消息(与真实飞书事件完全一致的路径)
|
|
930
|
+
const eventId = body.event.header?.event_id ?? `test-${Date.now()}`;
|
|
931
|
+
const message = await parseFeishuMessage(body.event, eventId);
|
|
932
|
+
// 2. 解析发送者名称(与 FeishuWSClient 完全一致的路径)
|
|
933
|
+
if (message.sender && deps.larkClient) {
|
|
934
|
+
message.senderName = await resolveFeishuUser(deps.larkClient, message.sender, db);
|
|
935
|
+
}
|
|
936
|
+
// 3. 入队处理
|
|
937
|
+
messageQueue.enqueue(message);
|
|
938
|
+
return reply.send({
|
|
939
|
+
ok: true,
|
|
940
|
+
injectedMessage: {
|
|
941
|
+
content: message.content,
|
|
942
|
+
sender: message.sender,
|
|
943
|
+
senderName: message.senderName,
|
|
944
|
+
chatId: message.chatId,
|
|
945
|
+
mentionsAnya: message.mentionsAnya,
|
|
946
|
+
isDirectMessage: message.isDirectMessage,
|
|
947
|
+
metadata: message.metadata,
|
|
948
|
+
},
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
// GET /api/messages/stream — SSE 实时推送
|
|
953
|
+
app.get('/api/messages/stream', async (request, reply) => {
|
|
954
|
+
reply.raw.writeHead(200, {
|
|
955
|
+
'Content-Type': 'text/event-stream',
|
|
956
|
+
'Cache-Control': 'no-cache',
|
|
957
|
+
'Connection': 'keep-alive',
|
|
958
|
+
});
|
|
959
|
+
// 发送初始连接确认
|
|
960
|
+
reply.raw.write('data: {"type":"connected"}\n\n');
|
|
961
|
+
const onMessage = (event) => {
|
|
962
|
+
reply.raw.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
963
|
+
};
|
|
964
|
+
messageEvents.on('message', onMessage);
|
|
965
|
+
// 客户端断开时清理 listener
|
|
966
|
+
request.raw.on('close', () => {
|
|
967
|
+
messageEvents.off('message', onMessage);
|
|
968
|
+
});
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* 尝试解析 detail JSON 字符串,失败则返回原始值
|
|
973
|
+
*/
|
|
974
|
+
function parseDetail(detail) {
|
|
975
|
+
if (detail === null || detail === undefined)
|
|
976
|
+
return null;
|
|
977
|
+
try {
|
|
978
|
+
return JSON.parse(detail);
|
|
979
|
+
}
|
|
980
|
+
catch {
|
|
981
|
+
return detail;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
function safeJsonParse(value) {
|
|
985
|
+
if (value === null || value === undefined)
|
|
986
|
+
return null;
|
|
987
|
+
try {
|
|
988
|
+
return JSON.parse(value);
|
|
989
|
+
}
|
|
990
|
+
catch {
|
|
991
|
+
return value;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
//# sourceMappingURL=http.js.map
|