team-anya 0.2.2 → 0.2.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.
@@ -1,10 +1,4 @@
1
- import { getRecentMessages } from '@team-anya/db';
2
1
  // ── 话术池 ──
3
- const SILENT_RESULT_REPLIES = [
4
- '刚才处理完了但好像漏说了结果,你再问我一下?',
5
- '不好意思,刚才干完活忘了汇报,你可以再@我一下',
6
- '处理完了但我好像没回你,抱歉,你再说一声我接上',
7
- ];
8
2
  const STUCK_REPLIES = [
9
3
  '我这边好像卡住了,正在尝试恢复,如果一直没反应你踢我一下',
10
4
  '抱歉,我可能卡在某个环节了,你可以发条消息唤醒我试试',
@@ -17,37 +11,28 @@ function pickRandom(pool) {
17
11
  /**
18
12
  * 静默健康监控器
19
13
  *
20
- * 正常流程零感知,仅在异常时主动通知用户:
21
- * 1. CC result 到了但没回复用户 → 兜底通知
22
- * 2. CC 实例卡死(executing 但长时间无 progress) → 告警
14
+ * 正常流程零感知,仅在 CC 实例卡死(executing 但长时间无 progress)时主动通知用户。
23
15
  */
24
16
  export class HealthMonitor {
25
17
  deps;
26
18
  config;
27
19
  logger;
28
20
  scanTimer = null;
29
- pendingChecks = new Map(); // instanceId → timer
30
21
  /** 已通知过卡死的实例,防止重复告警 */
31
22
  stuckNotified = new Set();
32
23
  constructor(deps, config = {}) {
33
24
  this.deps = deps;
34
25
  this.logger = deps.logger ?? { info: console.log, error: console.error };
35
26
  this.config = {
36
- resultCheckDelayMs: config.resultCheckDelayMs ?? 10_000,
37
27
  stuckThresholdMs: config.stuckThresholdMs ?? 3 * 60_000,
38
28
  scanIntervalMs: config.scanIntervalMs ?? 30_000,
39
- recentOutboundWindowMs: config.recentOutboundWindowMs ?? 30_000,
40
29
  };
41
30
  }
42
31
  /**
43
32
  * 启动监控:监听 broker 事件 + 启动定时扫描
44
33
  */
45
34
  start() {
46
- // 场景 2:监听 result 事件,延迟检查是否有回复
47
- this.deps.broker.on('instance.result', (instanceId, role, _result) => {
48
- this.scheduleResultCheck(instanceId, role);
49
- });
50
- // 场景 3:定时扫描卡死实例
35
+ // 定时扫描卡死实例
51
36
  this.scanTimer = setInterval(() => this.checkStuckInstances(), this.config.scanIntervalMs);
52
37
  // 实例退出时清理追踪状态
53
38
  this.deps.broker.on('instance.exited', (instanceId) => {
@@ -66,36 +51,10 @@ export class HealthMonitor {
66
51
  clearInterval(this.scanTimer);
67
52
  this.scanTimer = null;
68
53
  }
69
- for (const timer of this.pendingChecks.values()) {
70
- clearTimeout(timer);
71
- }
72
- this.pendingChecks.clear();
73
54
  this.stuckNotified.clear();
74
55
  this.logger.info('[HealthMonitor] 已停止');
75
56
  }
76
- // ── 场景 2:result 到了但没回复 ──
77
- scheduleResultCheck(instanceId, role) {
78
- // 如果已有待检查的 timer,先清掉(同一实例连续 result)
79
- const existing = this.pendingChecks.get(instanceId);
80
- if (existing)
81
- clearTimeout(existing);
82
- const timer = setTimeout(() => {
83
- this.pendingChecks.delete(instanceId);
84
- this.checkResultReply(instanceId, role);
85
- }, this.config.resultCheckDelayMs);
86
- this.pendingChecks.set(instanceId, timer);
87
- }
88
- checkResultReply(instanceId, role) {
89
- const chatId = this.deps.resolveChatId(instanceId, role);
90
- if (!chatId)
91
- return; // 无法定位用户,跳过
92
- if (this.hasRecentOutbound(chatId))
93
- return; // 已回复,正常
94
- // 没回复 → 发兜底通知
95
- this.notify(chatId, 'silent_result');
96
- this.logger.info(`[HealthMonitor] result 无回复兜底通知 (instance: ${instanceId}, chat: ${chatId})`);
97
- }
98
- // ── 场景 3:卡死检测 ──
57
+ // ── 卡死检测 ──
99
58
  checkStuckInstances() {
100
59
  const now = Date.now();
101
60
  const instances = this.deps.broker.status();
@@ -115,25 +74,13 @@ export class HealthMonitor {
115
74
  if (!chatId)
116
75
  continue;
117
76
  this.stuckNotified.add(inst.id);
118
- this.notify(chatId, 'stuck');
77
+ this.notify(chatId);
119
78
  this.logger.info(`[HealthMonitor] 卡死告警 (instance: ${inst.id}, idle: ${Math.round(elapsed / 1000)}s)`);
120
79
  }
121
80
  }
122
81
  // ── 工具方法 ──
123
- hasRecentOutbound(chatId) {
124
- const since = new Date(Date.now() - this.config.recentOutboundWindowMs).toISOString();
125
- const messages = getRecentMessages(this.deps.db, {
126
- direction: 'outbound',
127
- chat_id: chatId,
128
- since,
129
- limit: 1,
130
- });
131
- return messages.length > 0;
132
- }
133
- notify(chatId, type) {
134
- const text = type === 'silent_result'
135
- ? pickRandom(SILENT_RESULT_REPLIES)
136
- : pickRandom(STUCK_REPLIES);
82
+ notify(chatId) {
83
+ const text = pickRandom(STUCK_REPLIES);
137
84
  this.deps.feishuSender.sendText({
138
85
  receiveIdType: 'chat_id',
139
86
  receiveId: chatId,
@@ -143,11 +90,6 @@ export class HealthMonitor {
143
90
  });
144
91
  }
145
92
  cleanup(instanceId) {
146
- const timer = this.pendingChecks.get(instanceId);
147
- if (timer) {
148
- clearTimeout(timer);
149
- this.pendingChecks.delete(instanceId);
150
- }
151
93
  this.stuckNotified.delete(instanceId);
152
94
  }
153
95
  }
@@ -253,7 +253,11 @@
253
253
  | project.mode | string | `project` / `adhoc` |
254
254
  | project.repos | array | 仓库列表(project 模式,单仓库也是数组) |
255
255
 
256
- **约束**: Brief 只写做什么和标准,不写怎么做。派工三步走:`task.dispatch`(MCP) → bash 准备工作区 → `yor.spawn`(MCP),三步缺一不可。
256
+ **约束**: Brief 只写做什么和标准,不写怎么做。
257
+
258
+ **派工一步到位**:dispatch 成功后自动启动 Yor,无需再手动调用 `yor.spawn`。
259
+ 返回值中 `instance_id` 为 Yor 实例 ID,`spawn_error`(如有)表示自动启动失败。
260
+ dispatch 返回 status="blocked" 时不会启动 Yor,通知人类即可。
257
261
 
258
262
  ### task.lookup
259
263
 
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## 铁律
4
4
 
5
+ ### 禁止删除工作区
6
+
7
+ **绝对不要执行 `rm -rf` 或任何方式删除工作区目录。** 任务完成后工作区由系统管理,不由你清理。原因:
8
+ - 已完成的任务可能被 `task.escalate_to_topic` 升级为话题,需要 fork 源工作区
9
+ - 工作区保留用于审计追溯
10
+
5
11
  ### 定向消息必须回应
6
12
 
7
13
  @Anya 或私聊的消息就是定向消息。收到后必须有明确的下一步动作,**且所有回复必须通过 `channel.send` 发出**(纯文本输出对方看不到):
@@ -97,44 +103,42 @@ memory.search 没有结果?30 秒内组装调研 Brief → 派工 Yor → 回
97
103
 
98
104
  ## 派工流程
99
105
 
106
+ **一步派工**:调用 `task.dispatch(...)` 即可,Yor 会自动启动。
107
+
100
108
  ```
101
- 1. task.dispatch(MCP) 创建任务 + 写 brief + 自动准备隔离工作区 → 返回 working_dir
102
- 2. yor.spawn(MCP) → 启动 Yor,传入 working_dir 和 brief_path
109
+ result = task.dispatch({ title, brief, project_id, ... })
110
+ // Yor 已自动启动,result.instance_id Yor 实例 ID
111
+ channel.send({ message: "已派工,稍等" })
103
112
  ```
104
113
 
105
- ### Step 1: 创建任务 + 准备工作区
114
+ ### 返回值
106
115
 
107
- 调用 `task.dispatch(...)`,返回:
108
116
  - `task_id`:任务 ID
109
117
  - `brief_path`:brief 文件绝对路径
110
118
  - `working_dir`:隔离工作区绝对路径
111
119
  - `status`:`dispatched`(成功)或 `blocked`(工作区准备失败)
112
- - `error`:失败原因(仅 status=blocked)
120
+ - `instance_id`:Yor 实例 ID(成功时)
121
+ - `spawn_error`:Yor 启动失败原因(任务已创建但 Yor 未启动,可手动调 `yor.spawn` 重试)
122
+ - `error`:工作区准备失败原因(仅 status=blocked)
113
123
 
114
124
  工作区自动准备:
115
125
  - **project 模式**:自动 clone --local 到 `{workspaceDir}/{repo_name}`
116
126
  - **adhoc 模式**:自动 mkdir 到 `{taskDir}/adhoc`
117
127
 
118
- ### Step 2: 启动 Yor
119
-
120
- 调用 `yor.spawn(...)`,**必须原样传入 dispatch 返回的 `working_dir` 和 `brief_path`**。
121
-
122
- > **⚠️ 严禁修改 working_dir!** 不要传 repo 子目录(如 `WS-xxx/test-repo`),必须传工作区根目录(`WS-xxx/`)。工作区根目录包含 Yor 的角色文件(CLAUDE.md、PROFILE.md 等),Yor 启动后会自动在子目录中操作代码。
128
+ ### 异常处理
123
129
 
124
- dispatch 返回 status=blocked 时,不执行 yor.spawn——通知人类工作区准备失败的原因。
130
+ - `status=blocked` 通知人类工作区准备失败的原因
131
+ - `spawn_error` → 任务已创建(READY),可手动调用 `yor.spawn({ task_id, working_dir, brief_path })` 重试
125
132
 
126
133
  ## 审核闭环
127
134
 
128
135
  Yor 完成任务后调用 `task.deliver`,CC 实例保持存活等待审核:
129
136
 
130
- - **通过** → `yor.approve()` + `delivery.submit()` + `task.update({ status: "DONE" })` + `channel.send()` + 清理工作区
137
+ - **通过** → `yor.approve()` + `delivery.submit()` + `task.update({ status: "DONE" })` + `channel.send()`
131
138
  - **返工** → `yor.rework({ task_id, feedback })` — Yor 在原工作区继续,修改完后再次 deliver
132
139
  - **升级** → `task.update({ status: "BLOCKED" })` + `yor.kill()` + `channel.send()` 告知人类
133
140
 
134
- 清理规则:
135
- - 通过:`yor.approve` 自动关闭 Yor;`rm -rf` 清理工作区,分支保留等 PR 合并
136
- - 返工:不清理工作区
137
- - 升级:`yor.kill` 关闭 Yor;不清理工作区
141
+ ⚠️ **任何审核结果都不清理工作区。** `yor.approve` 自动关闭 Yor CC 实例即可,工作区目录保留。
138
142
 
139
143
  ## 场景打法
140
144
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-anya",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "description": "Team Anya - AI 数字员工系统",
6
6
  "bin": {
@@ -460,8 +460,10 @@ Brief 只写做什么和标准,不写怎么做。
460
460
  同时从当前对话上下文中提取 created_by(sender_name 或 sender_id)和 source_chat_id(chat_id),确保任务可追溯。
461
461
 
462
462
  返回值包含 task_id、brief_path 和 working_dir(已准备好的隔离工作区绝对路径)。
463
- 派工两步走:task.dispatch(MCP) → yor.spawn(MCP,传入返回的 working_dir 和 brief_path)。
464
- 如果返回 status="blocked",说明工作区准备失败,error 字段包含原因,此时不要调用 yor.spawn,应通知人类。`,
463
+
464
+ 派工成功(status="dispatched")后会自动启动 Yor,无需再调用 yor.spawn。返回值中 instance_id 为 Yor 实例 ID。
465
+ 如果返回 status="blocked",说明工作区准备失败,error 字段包含原因,应通知人类。
466
+ 如果返回 spawn_error,说明 Yor 启动失败但任务已创建(READY),可手动调用 yor.spawn 重试。`,
465
467
  inputSchema: TaskDispatchInputSchema,
466
468
  layer: 2,
467
469
  roles: ['loid'],
@@ -795,13 +797,33 @@ export function createToolRouter(role, deps) {
795
797
  if (!deps.workspaceManager)
796
798
  throw new Error('workspaceManager 未注入');
797
799
  const { taskDispatch } = await import('./layer2/loid/task-dispatch.js');
798
- return taskDispatch({
800
+ const dispatchResult = await taskDispatch({
799
801
  db: deps.db,
800
802
  workspacePath: deps.workspacePath,
801
803
  logger: deps.logger,
802
804
  getProjectConfig: deps.getProjectConfig,
803
805
  workspaceManager: deps.workspaceManager,
804
806
  }, args);
807
+ // 派工成功 → 自动 spawn Yor(固定流程,不需要 LLM 判断)
808
+ if (dispatchResult.status === 'dispatched' && dispatchResult.working_dir) {
809
+ if (!deps.yorOrchestrator) {
810
+ deps.logger?.error('[anya:pipeline] [dispatch] auto-spawn 跳过: yorOrchestrator 未注入');
811
+ }
812
+ else {
813
+ try {
814
+ const { yorSpawn } = await import('./layer2/loid/yor-spawn.js');
815
+ const spawnResult = await yorSpawn({ yorOrchestrator: deps.yorOrchestrator, logger: deps.logger }, { task_id: dispatchResult.task_id, working_dir: dispatchResult.working_dir, brief_path: dispatchResult.brief_path });
816
+ dispatchResult.instance_id = spawnResult.instance_id;
817
+ deps.logger?.info(`[anya:pipeline] [dispatch] auto-spawn 完成: ${spawnResult.instance_id}`);
818
+ }
819
+ catch (err) {
820
+ const msg = err instanceof Error ? err.message : String(err);
821
+ deps.logger?.error(`[anya:pipeline] [dispatch] auto-spawn 失败: ${msg}`);
822
+ dispatchResult.spawn_error = msg;
823
+ }
824
+ }
825
+ }
826
+ return dispatchResult;
805
827
  }
806
828
  case 'task.lookup': {
807
829
  const { taskLookup } = await import('./layer2/loid/task-lookup.js');