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
|
-
//
|
|
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
|
-
// ──
|
|
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
|
|
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
|
-
|
|
124
|
-
const
|
|
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
|
}
|
package/blueprint/TOOLS.md
CHANGED
|
@@ -253,7 +253,11 @@
|
|
|
253
253
|
| project.mode | string | `project` / `adhoc` |
|
|
254
254
|
| project.repos | array | 仓库列表(project 模式,单仓库也是数组) |
|
|
255
255
|
|
|
256
|
-
**约束**: Brief
|
|
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
|
-
|
|
102
|
-
|
|
109
|
+
result = task.dispatch({ title, brief, project_id, ... })
|
|
110
|
+
// Yor 已自动启动,result.instance_id 为 Yor 实例 ID
|
|
111
|
+
channel.send({ message: "已派工,稍等" })
|
|
103
112
|
```
|
|
104
113
|
|
|
105
|
-
###
|
|
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
|
-
- `
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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');
|