team-anya-cli 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/server/dist/cli.js +1 -1
- package/apps/server/dist/gateway/feishu-ws.js +1 -0
- package/apps/server/dist/gateway/http.js +25 -0
- package/apps/server/dist/gateway/message-queue.js +14 -0
- package/apps/server/dist/loid/mcp-server.js +1 -0
- package/apps/server/dist/main.js +1 -0
- package/package.json +1 -1
- package/packages/db/dist/index.js +4 -4
- package/packages/db/dist/schema/audit-events.js +1 -1
- package/packages/db/dist/schema/cc-sessions.js +1 -1
- package/packages/db/dist/schema/chats.js +3 -3
- package/packages/db/dist/schema/commitments.js +1 -1
- package/packages/db/dist/schema/communication-events.js +1 -1
- package/packages/db/dist/schema/message-log.js +1 -1
- package/packages/db/dist/schema/opportunities.js +1 -1
- package/packages/db/dist/schema/org.js +1 -1
- package/packages/db/dist/schema/projects.js +3 -3
- package/packages/db/dist/schema/tasks.js +3 -3
- package/packages/db/dist/schema/trace-spans.js +1 -1
- package/packages/mcp-tools/dist/layer2/loid/task-dispatch.js +19 -0
- package/packages/mcp-tools/dist/registry.js +6 -2
package/apps/server/dist/cli.js
CHANGED
|
@@ -201,7 +201,7 @@ program
|
|
|
201
201
|
});
|
|
202
202
|
console.log('\n Anya Setup - 交互式配置向导\n');
|
|
203
203
|
// ── 1. ANYA_HOME ──
|
|
204
|
-
const anyaHome = resolveHome(await ask('数据目录 ANYA_HOME', opts.home ?? '~/.anya'));
|
|
204
|
+
const anyaHome = resolveHome(await ask('数据目录 ANYA_HOME', opts.home ?? process.env.ANYA_HOME ?? '~/.anya'));
|
|
205
205
|
// 创建目录结构
|
|
206
206
|
const dirs = ['data', 'data/logs', 'office', 'repos', 'media/feishu'];
|
|
207
207
|
for (const dir of dirs)
|
|
@@ -25,6 +25,7 @@ export async function parseFeishuMessage(event, eventId, downloader) {
|
|
|
25
25
|
message_id: msg.message_id,
|
|
26
26
|
message_type: msg.message_type,
|
|
27
27
|
source_type: 'feishu',
|
|
28
|
+
...(event.header?.create_time ? { event_create_time: event.header.create_time } : {}),
|
|
28
29
|
...(msg.parent_id ? { parent_message_id: msg.parent_id } : {}),
|
|
29
30
|
...(msg.root_id ? { root_message_id: msg.root_id } : {}),
|
|
30
31
|
},
|
|
@@ -10,9 +10,34 @@ import { Clarifier } from '../loid/clarifier.js';
|
|
|
10
10
|
import { OpportunityManager } from '../loid/opportunity-manager.js';
|
|
11
11
|
import { SelfCalibrator } from '../loid/self-calibrator.js';
|
|
12
12
|
import { parseFeishuMessage, resolveFeishuUser } from './feishu-ws.js';
|
|
13
|
+
/**
|
|
14
|
+
* 将 SQLite datetime('now') 产生的 'YYYY-MM-DD HH:MM:SS'(隐含 UTC)
|
|
15
|
+
* 统一转为 ISO 8601 'YYYY-MM-DDTHH:MM:SSZ',确保前端正确按 UTC 解析。
|
|
16
|
+
*/
|
|
17
|
+
const UTC_DATETIME_RE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
|
|
18
|
+
function normalizeTimestamps(obj) {
|
|
19
|
+
if (typeof obj === 'string') {
|
|
20
|
+
return UTC_DATETIME_RE.test(obj) ? obj.replace(' ', 'T') + 'Z' : obj;
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(obj)) {
|
|
23
|
+
return obj.map(normalizeTimestamps);
|
|
24
|
+
}
|
|
25
|
+
if (obj !== null && typeof obj === 'object') {
|
|
26
|
+
const result = {};
|
|
27
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
28
|
+
result[key] = normalizeTimestamps(value);
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
return obj;
|
|
33
|
+
}
|
|
13
34
|
export async function registerRoutes(app, deps) {
|
|
14
35
|
const { db, broker } = deps;
|
|
15
36
|
const clarifier = deps.clarifier ?? new Clarifier({ db });
|
|
37
|
+
// 统一序列化钩子:API 响应中的 UTC 时间戳标准化
|
|
38
|
+
app.addHook('preSerialization', async (_request, _reply, payload) => {
|
|
39
|
+
return normalizeTimestamps(payload);
|
|
40
|
+
});
|
|
16
41
|
// POST /api/tasks — 创建任务
|
|
17
42
|
app.post('/api/tasks', async (request, reply) => {
|
|
18
43
|
const body = request.body;
|
|
@@ -31,14 +31,19 @@ class EventDedup {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
// ── MessageQueue ──
|
|
35
|
+
/** 消息最大年龄(毫秒),超过则丢弃。默认 5 分钟。 */
|
|
36
|
+
const DEFAULT_MAX_AGE_MS = 5 * 60 * 1000;
|
|
34
37
|
export class MessageQueue {
|
|
35
38
|
handler;
|
|
36
39
|
dedup;
|
|
40
|
+
maxAgeMs;
|
|
37
41
|
sources = new Map();
|
|
38
42
|
logger;
|
|
39
43
|
constructor(config) {
|
|
40
44
|
this.handler = config.handler;
|
|
41
45
|
this.dedup = new EventDedup(config.dedupTtlMs ?? 5 * 60 * 1000);
|
|
46
|
+
this.maxAgeMs = config.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
42
47
|
this.logger = config.logger ?? { info: console.log, error: console.error };
|
|
43
48
|
}
|
|
44
49
|
enqueue(msg) {
|
|
@@ -47,6 +52,15 @@ export class MessageQueue {
|
|
|
47
52
|
// 重复事件,静默跳过
|
|
48
53
|
return;
|
|
49
54
|
}
|
|
55
|
+
// 丢弃过旧消息(飞书重连后可能重发很久以前的事件)
|
|
56
|
+
const createTime = msg.metadata?.event_create_time;
|
|
57
|
+
if (createTime) {
|
|
58
|
+
const ageMs = Date.now() - parseInt(createTime, 10);
|
|
59
|
+
if (ageMs > this.maxAgeMs) {
|
|
60
|
+
this.logger.warn?.(`[MessageQueue] 丢弃过旧消息: event_id=${eventId}, age=${Math.round(ageMs / 1000)}s, max=${Math.round(this.maxAgeMs / 1000)}s`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
50
64
|
const key = this.deriveSourceKey(msg);
|
|
51
65
|
let sq = this.sources.get(key);
|
|
52
66
|
if (!sq) {
|
|
@@ -15,6 +15,7 @@ function createMcpServer(deps) {
|
|
|
15
15
|
onMessageSent: deps.onMessageSent,
|
|
16
16
|
yorOrchestrator: deps.yorOrchestrator,
|
|
17
17
|
getProjectConfig: deps.getProjectConfig,
|
|
18
|
+
onProjectChanged: deps.onProjectChanged,
|
|
18
19
|
};
|
|
19
20
|
const router = createToolRouter('loid', routerDeps);
|
|
20
21
|
const server = new Server({ name: 'loid-mcp', version: '3.0.0' }, { capabilities: { tools: {} } });
|
package/apps/server/dist/main.js
CHANGED
|
@@ -173,6 +173,7 @@ export async function buildServer() {
|
|
|
173
173
|
logger: app.log,
|
|
174
174
|
yorOrchestrator,
|
|
175
175
|
getProjectConfig: (projectId) => worktreeManager.getProjectConfig(projectId),
|
|
176
|
+
onProjectChanged: () => worktreeManager.invalidateCache(),
|
|
176
177
|
},
|
|
177
178
|
});
|
|
178
179
|
await loidBrain.init();
|
package/package.json
CHANGED
|
@@ -124,7 +124,7 @@ export function getOrgMember(db, memberId) {
|
|
|
124
124
|
export function upsertOrgMember(db, data) {
|
|
125
125
|
const setFields = {
|
|
126
126
|
name: data.name,
|
|
127
|
-
updated_at: sql `(
|
|
127
|
+
updated_at: sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`,
|
|
128
128
|
};
|
|
129
129
|
if (data.platform !== undefined)
|
|
130
130
|
setFields.platform = data.platform;
|
|
@@ -354,7 +354,7 @@ export function upsertChat(db, data) {
|
|
|
354
354
|
bot_count: data.bot_count,
|
|
355
355
|
metadata: data.metadata,
|
|
356
356
|
last_synced_at: data.last_synced_at,
|
|
357
|
-
updated_at: sql `(
|
|
357
|
+
updated_at: sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`,
|
|
358
358
|
},
|
|
359
359
|
})
|
|
360
360
|
.returning()
|
|
@@ -391,7 +391,7 @@ export function upsertChatMember(db, data) {
|
|
|
391
391
|
.get();
|
|
392
392
|
if (existing) {
|
|
393
393
|
return db.update(chatMembers)
|
|
394
|
-
.set({ role: data.role ?? existing.role, synced_at: sql `(
|
|
394
|
+
.set({ role: data.role ?? existing.role, synced_at: sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))` })
|
|
395
395
|
.where(eq(chatMembers.id, existing.id))
|
|
396
396
|
.returning()
|
|
397
397
|
.get();
|
|
@@ -452,7 +452,7 @@ export function upsertProject(db, data) {
|
|
|
452
452
|
description: data.description,
|
|
453
453
|
platform: data.platform,
|
|
454
454
|
claude_md: data.claude_md,
|
|
455
|
-
updated_at: sql `(
|
|
455
|
+
updated_at: sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`,
|
|
456
456
|
},
|
|
457
457
|
})
|
|
458
458
|
.returning()
|
|
@@ -8,6 +8,6 @@ export const auditEvents = sqliteTable('audit_events', {
|
|
|
8
8
|
summary: text('summary').notNull(),
|
|
9
9
|
detail: text('detail'), // JSON 字符串
|
|
10
10
|
file_ref: text('file_ref'),
|
|
11
|
-
created_at: text('created_at').notNull().default(sql `(
|
|
11
|
+
created_at: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`),
|
|
12
12
|
});
|
|
13
13
|
//# sourceMappingURL=audit-events.js.map
|
|
@@ -8,7 +8,7 @@ export const ccSessions = sqliteTable('cc_sessions', {
|
|
|
8
8
|
task_id: text('task_id'),
|
|
9
9
|
chat_id: text('chat_id'),
|
|
10
10
|
project_path: text('project_path'),
|
|
11
|
-
started_at: text('started_at').notNull().default(sql `(
|
|
11
|
+
started_at: text('started_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`),
|
|
12
12
|
ended_at: text('ended_at'),
|
|
13
13
|
});
|
|
14
14
|
//# sourceMappingURL=cc-sessions.js.map
|
|
@@ -18,9 +18,9 @@ export const chats = sqliteTable('chats', {
|
|
|
18
18
|
user_count: integer('user_count'),
|
|
19
19
|
bot_count: integer('bot_count'),
|
|
20
20
|
metadata: text('metadata'), // JSON 扩展
|
|
21
|
-
first_seen_at: text('first_seen_at').notNull().default(sql `(
|
|
21
|
+
first_seen_at: text('first_seen_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`),
|
|
22
22
|
last_synced_at: text('last_synced_at'),
|
|
23
|
-
updated_at: text('updated_at').notNull().default(sql `(
|
|
23
|
+
updated_at: text('updated_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`),
|
|
24
24
|
});
|
|
25
25
|
// chat_members 关联表 — 群 ↔ 人 多对多
|
|
26
26
|
export const chatMembers = sqliteTable('chat_members', {
|
|
@@ -28,6 +28,6 @@ export const chatMembers = sqliteTable('chat_members', {
|
|
|
28
28
|
chat_id: text('chat_id').notNull().references(() => chats.chat_id),
|
|
29
29
|
member_id: text('member_id').notNull().references(() => orgMembers.member_id),
|
|
30
30
|
role: text('role').default('member'), // owner / admin / member
|
|
31
|
-
synced_at: text('synced_at').notNull().default(sql `(
|
|
31
|
+
synced_at: text('synced_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`),
|
|
32
32
|
});
|
|
33
33
|
//# sourceMappingURL=chats.js.map
|
|
@@ -13,6 +13,6 @@ export const commitments = sqliteTable('commitments', {
|
|
|
13
13
|
deadline: text('deadline'),
|
|
14
14
|
status: text('status').notNull().default('active'),
|
|
15
15
|
break_reason: text('break_reason'),
|
|
16
|
-
promised_at: text('promised_at').notNull().default(sql `(
|
|
16
|
+
promised_at: text('promised_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`),
|
|
17
17
|
});
|
|
18
18
|
//# sourceMappingURL=commitments.js.map
|
|
@@ -9,6 +9,6 @@ export const communicationEvents = sqliteTable('communication_events', {
|
|
|
9
9
|
summary: text('summary').notNull(),
|
|
10
10
|
raw_content_path: text('raw_content_path'),
|
|
11
11
|
opportunity_id: text('opportunity_id'),
|
|
12
|
-
created_at: text('created_at').notNull().default(sql `(
|
|
12
|
+
created_at: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`),
|
|
13
13
|
});
|
|
14
14
|
//# sourceMappingURL=communication-events.js.map
|
|
@@ -15,6 +15,6 @@ export const messageLog = sqliteTable('message_log', {
|
|
|
15
15
|
related_task_id: text('related_task_id'),
|
|
16
16
|
metadata: text('metadata'), // JSON
|
|
17
17
|
trace_id: text('trace_id'),
|
|
18
|
-
created_at: text('created_at').notNull().default(sql `(
|
|
18
|
+
created_at: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`),
|
|
19
19
|
});
|
|
20
20
|
//# sourceMappingURL=message-log.js.map
|
|
@@ -17,7 +17,7 @@ export const opportunities = sqliteTable('opportunities', {
|
|
|
17
17
|
score_permission: real('score_permission'),
|
|
18
18
|
total_score: real('total_score'),
|
|
19
19
|
converted_task_id: text('converted_task_id').references(() => tasks.task_id),
|
|
20
|
-
detected_at: text('detected_at').notNull().default(sql `(
|
|
20
|
+
detected_at: text('detected_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`),
|
|
21
21
|
claimed_at: text('claimed_at'),
|
|
22
22
|
});
|
|
23
23
|
//# sourceMappingURL=opportunities.js.map
|
|
@@ -17,7 +17,7 @@ export const orgMembers = sqliteTable('org_members', {
|
|
|
17
17
|
avatar_url: text('avatar_url'),
|
|
18
18
|
metadata: text('metadata'), // JSON 扩展
|
|
19
19
|
last_synced_at: text('last_synced_at'),
|
|
20
|
-
updated_at: text('updated_at').notNull().default(sql `(
|
|
20
|
+
updated_at: text('updated_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`),
|
|
21
21
|
});
|
|
22
22
|
export const orgOwnership = sqliteTable('org_ownership', {
|
|
23
23
|
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
@@ -6,8 +6,8 @@ export const projects = sqliteTable('projects', {
|
|
|
6
6
|
description: text('description'),
|
|
7
7
|
platform: text('platform').default('github'), // github | gitlab | local
|
|
8
8
|
claude_md: text('claude_md'),
|
|
9
|
-
created_at: text('created_at').notNull().default(sql `(
|
|
10
|
-
updated_at: text('updated_at').notNull().default(sql `(
|
|
9
|
+
created_at: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`),
|
|
10
|
+
updated_at: text('updated_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`),
|
|
11
11
|
deleted_at: text('deleted_at'), // null = 活跃,有值 = 已软删除
|
|
12
12
|
});
|
|
13
13
|
// project_repos 关联表 — 项目 ↔ 仓库 一对多
|
|
@@ -18,6 +18,6 @@ export const projectRepos = sqliteTable('project_repos', {
|
|
|
18
18
|
git_url: text('git_url'), // 远程仓库地址,用于自动 clone
|
|
19
19
|
repo_path: text('repo_path'), // 主仓库绝对路径
|
|
20
20
|
default_branch: text('default_branch').default('main'),
|
|
21
|
-
created_at: text('created_at').notNull().default(sql `(
|
|
21
|
+
created_at: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`),
|
|
22
22
|
});
|
|
23
23
|
//# sourceMappingURL=projects.js.map
|
|
@@ -30,8 +30,8 @@ export const tasks = sqliteTable('tasks', {
|
|
|
30
30
|
// 文件路径
|
|
31
31
|
workspace_path: text('workspace_path'),
|
|
32
32
|
// 时间戳
|
|
33
|
-
created_at: text('created_at').notNull().default(sql `(
|
|
34
|
-
updated_at: text('updated_at').notNull().default(sql `(
|
|
33
|
+
created_at: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`),
|
|
34
|
+
updated_at: text('updated_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`),
|
|
35
35
|
});
|
|
36
36
|
export const taskClarifications = sqliteTable('task_clarifications', {
|
|
37
37
|
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
@@ -40,7 +40,7 @@ export const taskClarifications = sqliteTable('task_clarifications', {
|
|
|
40
40
|
answer: text('answer'),
|
|
41
41
|
asked_by: text('asked_by').notNull().default('loid'),
|
|
42
42
|
answered_by: text('answered_by'),
|
|
43
|
-
asked_at: text('asked_at').notNull().default(sql `(
|
|
43
|
+
asked_at: text('asked_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`),
|
|
44
44
|
answered_at: text('answered_at'),
|
|
45
45
|
});
|
|
46
46
|
//# sourceMappingURL=tasks.js.map
|
|
@@ -14,6 +14,6 @@ export const traceSpans = sqliteTable('trace_spans', {
|
|
|
14
14
|
output: text('output'), // JSON
|
|
15
15
|
error: text('error'),
|
|
16
16
|
metadata: text('metadata'), // JSON
|
|
17
|
-
created_at: text('created_at').notNull().default(sql `(
|
|
17
|
+
created_at: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))`),
|
|
18
18
|
});
|
|
19
19
|
//# sourceMappingURL=trace-spans.js.map
|
|
@@ -47,6 +47,25 @@ export async function taskDispatch(deps, input) {
|
|
|
47
47
|
const project = deps.getProjectConfig
|
|
48
48
|
? await deps.getProjectConfig(input.project_id)
|
|
49
49
|
: undefined;
|
|
50
|
+
// 传了 project_id 但未在注册表中找到 → 直接 BLOCKED,不静默降级为 adhoc
|
|
51
|
+
if (input.project_id && project?.mode === 'adhoc') {
|
|
52
|
+
logger?.info(`[anya:pipeline] [Loid] 创建任务 ${taskId} "${input.title}" | project=${input.project_id} 未注册,BLOCKED`);
|
|
53
|
+
assertTransition(TaskStatus.NEW, TaskStatus.BLOCKED);
|
|
54
|
+
updateTask(db, taskId, { status: TaskStatus.BLOCKED });
|
|
55
|
+
insertAuditEvent(db, {
|
|
56
|
+
event_type: 'task_status_changed',
|
|
57
|
+
actor: 'loid',
|
|
58
|
+
task_id: taskId,
|
|
59
|
+
summary: `NEW → BLOCKED: 项目 "${input.project_id}" 未在注册表中找到,请先通过 project.upsert 注册`,
|
|
60
|
+
detail: JSON.stringify({ from: 'NEW', to: 'BLOCKED', reason: 'project_not_found', project_id: input.project_id }),
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
task_id: taskId,
|
|
64
|
+
status: 'blocked',
|
|
65
|
+
brief_path: briefPath,
|
|
66
|
+
error: `项目 "${input.project_id}" 未在注册表中找到。请先调用 project.upsert 注册项目及其仓库配置,然后重新派工。`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
50
69
|
logger?.info(`[anya:pipeline] [Loid] 创建任务 ${taskId} "${input.title}"${input.project_id ? ` | project=${input.project_id} mode=${project?.mode}` : ''}`);
|
|
51
70
|
// 5. 自动准备工作区
|
|
52
71
|
const workspaceResult = await prepareWorkspace(taskId, taskDir, project, logger);
|
|
@@ -644,11 +644,15 @@ export function createToolRouter(role, deps) {
|
|
|
644
644
|
// Layer 2: Loid 专属 - 项目管理
|
|
645
645
|
case 'project.upsert': {
|
|
646
646
|
const { projectUpsert } = await import('./layer2/loid/project-upsert.js');
|
|
647
|
-
|
|
647
|
+
const result = await projectUpsert(deps.db, deps.workspacePath, args);
|
|
648
|
+
deps.onProjectChanged?.();
|
|
649
|
+
return result;
|
|
648
650
|
}
|
|
649
651
|
case 'project.remove': {
|
|
650
652
|
const { projectRemove } = await import('./layer2/loid/project-remove.js');
|
|
651
|
-
|
|
653
|
+
const result = await projectRemove(deps.db, deps.workspacePath, args);
|
|
654
|
+
deps.onProjectChanged?.();
|
|
655
|
+
return result;
|
|
652
656
|
}
|
|
653
657
|
// Layer 2: Loid 专属 - yor/delivery
|
|
654
658
|
case 'yor.spawn': {
|