team-anya-cli 0.1.3 → 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.
@@ -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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-anya-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "Team Anya - AI 数字员工系统",
6
6
  "bin": {
@@ -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 `(datetime('now'))`,
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 `(datetime('now'))`,
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 `(datetime('now'))` })
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 `(datetime('now'))`,
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 `(datetime('now'))`),
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 `(datetime('now'))`),
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 `(datetime('now'))`),
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 `(datetime('now'))`),
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 `(datetime('now'))`),
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 `(datetime('now'))`),
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 `(datetime('now'))`),
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 `(datetime('now'))`),
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 `(datetime('now'))`),
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 `(datetime('now'))`),
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 `(datetime('now'))`),
10
- updated_at: text('updated_at').notNull().default(sql `(datetime('now'))`),
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 `(datetime('now'))`),
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 `(datetime('now'))`),
34
- updated_at: text('updated_at').notNull().default(sql `(datetime('now'))`),
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 `(datetime('now'))`),
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 `(datetime('now'))`),
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