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,257 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { ClaudeCodeBackend } from '@team-anya/cc-client';
|
|
3
|
+
import { ConnectionState, ScopeChecker, } from '@team-anya/core';
|
|
4
|
+
// ── CCBroker ──
|
|
5
|
+
/**
|
|
6
|
+
* CCBroker:统一 CC 进程管理器
|
|
7
|
+
*
|
|
8
|
+
* 管理所有 ClaudeCodeBackend 实例(Loid + Yor),提供:
|
|
9
|
+
* - 生命周期管理(spawn / dispose / shutdown)
|
|
10
|
+
* - 消息路由(sendPrompt)
|
|
11
|
+
* - 事件通知(result / crash / progress)
|
|
12
|
+
* - 并发控制(按角色分开限制)
|
|
13
|
+
*/
|
|
14
|
+
export class CCBroker extends EventEmitter {
|
|
15
|
+
instances = new Map();
|
|
16
|
+
maxLoidInstances;
|
|
17
|
+
maxYorInstances;
|
|
18
|
+
logger;
|
|
19
|
+
shuttingDown = false;
|
|
20
|
+
constructor(config) {
|
|
21
|
+
super();
|
|
22
|
+
this.maxLoidInstances = config.maxLoidInstances;
|
|
23
|
+
this.maxYorInstances = config.maxYorInstances;
|
|
24
|
+
this.logger = config.logger ?? { info: console.log, error: console.error };
|
|
25
|
+
}
|
|
26
|
+
// ── 生命周期 ──
|
|
27
|
+
/**
|
|
28
|
+
* 启动一个新的 CC 实例
|
|
29
|
+
*/
|
|
30
|
+
async spawn(options) {
|
|
31
|
+
if (this.shuttingDown) {
|
|
32
|
+
throw new Error('CCBroker 正在关闭,无法 spawn 新实例');
|
|
33
|
+
}
|
|
34
|
+
// 并发检查
|
|
35
|
+
const currentCount = this.countByRole(options.role);
|
|
36
|
+
const maxCount = options.role === 'loid' ? this.maxLoidInstances : this.maxYorInstances;
|
|
37
|
+
if (currentCount >= maxCount) {
|
|
38
|
+
throw new Error(`${options.role} 实例已满(${currentCount}/${maxCount})`);
|
|
39
|
+
}
|
|
40
|
+
// 重复 ID 检查
|
|
41
|
+
if (this.instances.has(options.id)) {
|
|
42
|
+
throw new Error(`实例 ${options.id} 已存在`);
|
|
43
|
+
}
|
|
44
|
+
const backend = new ClaudeCodeBackend({
|
|
45
|
+
logFile: options.logFile,
|
|
46
|
+
});
|
|
47
|
+
const instance = {
|
|
48
|
+
id: options.id,
|
|
49
|
+
role: options.role,
|
|
50
|
+
backend,
|
|
51
|
+
state: 'connecting',
|
|
52
|
+
config: options.backendConfig,
|
|
53
|
+
mcpServer: options.mcpServer,
|
|
54
|
+
taskId: options.taskId,
|
|
55
|
+
createdAt: new Date(),
|
|
56
|
+
lastActivityAt: new Date(),
|
|
57
|
+
};
|
|
58
|
+
// 注册 Scope 检查器(Yor 实例)
|
|
59
|
+
if (options.scope) {
|
|
60
|
+
instance.scopeChecker = new ScopeChecker(options.scope);
|
|
61
|
+
backend.onToolRequest(instance.scopeChecker.createHandler());
|
|
62
|
+
}
|
|
63
|
+
// 注册 session ID 回调
|
|
64
|
+
backend.onSessionId((sessionId) => {
|
|
65
|
+
instance.cliSessionId = sessionId;
|
|
66
|
+
this.emit('instance.sessionId', instance.id, sessionId);
|
|
67
|
+
});
|
|
68
|
+
// 注册事件回调
|
|
69
|
+
backend.onResult((result) => {
|
|
70
|
+
instance.lastActivityAt = new Date();
|
|
71
|
+
instance.state = 'idle';
|
|
72
|
+
this.emit('instance.result', instance.id, instance.role, result);
|
|
73
|
+
});
|
|
74
|
+
backend.onProgress((event) => {
|
|
75
|
+
instance.lastActivityAt = new Date();
|
|
76
|
+
this.emit('instance.progress', instance.id, event);
|
|
77
|
+
});
|
|
78
|
+
backend.onStateChange((connState) => {
|
|
79
|
+
if (connState === ConnectionState.CONNECTED) {
|
|
80
|
+
if (instance.state === 'connecting') {
|
|
81
|
+
instance.state = 'connected';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (connState === ConnectionState.EXECUTING) {
|
|
85
|
+
instance.state = 'executing';
|
|
86
|
+
}
|
|
87
|
+
else if (connState === ConnectionState.ERROR || connState === ConnectionState.DISCONNECTED) {
|
|
88
|
+
if (instance.state !== 'disposed' && instance.state !== 'connecting') {
|
|
89
|
+
instance.state = 'error';
|
|
90
|
+
this.emit('instance.crash', instance.id, instance.role, new Error(`CC 实例 ${instance.id} 连接断开 (state=${connState})`));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
// 存入 Map 后连接(这样事件回调生效)
|
|
95
|
+
this.instances.set(options.id, instance);
|
|
96
|
+
try {
|
|
97
|
+
await backend.connect(options.backendConfig);
|
|
98
|
+
instance.state = 'connected';
|
|
99
|
+
this.logger.info(`[CCBroker] 实例 ${options.id} (${options.role}) 已连接`);
|
|
100
|
+
return instance;
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
// 连接失败,清理
|
|
104
|
+
this.instances.delete(options.id);
|
|
105
|
+
if (options.mcpServer) {
|
|
106
|
+
await options.mcpServer.close().catch(() => { });
|
|
107
|
+
}
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 向指定实例注入 prompt
|
|
113
|
+
*/
|
|
114
|
+
sendPrompt(instanceId, prompt) {
|
|
115
|
+
const instance = this.instances.get(instanceId);
|
|
116
|
+
if (!instance) {
|
|
117
|
+
throw new Error(`实例 ${instanceId} 不存在`);
|
|
118
|
+
}
|
|
119
|
+
if (instance.state === 'disposed') {
|
|
120
|
+
throw new Error(`实例 ${instanceId} 已释放`);
|
|
121
|
+
}
|
|
122
|
+
instance.lastActivityAt = new Date();
|
|
123
|
+
instance.state = 'executing';
|
|
124
|
+
// sendPrompt 是异步的但不需要等待
|
|
125
|
+
instance.backend.sendPrompt(prompt).catch((err) => {
|
|
126
|
+
this.logger.error(`[CCBroker] sendPrompt 失败 (${instanceId}):`, err);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 注册工具请求拦截器(可用于外部 scope 检查或自定义逻辑)
|
|
131
|
+
*/
|
|
132
|
+
onToolRequest(instanceId, handler) {
|
|
133
|
+
const instance = this.instances.get(instanceId);
|
|
134
|
+
if (!instance) {
|
|
135
|
+
throw new Error(`实例 ${instanceId} 不存在`);
|
|
136
|
+
}
|
|
137
|
+
instance.backend.onToolRequest(handler);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* 释放指定实例
|
|
141
|
+
*/
|
|
142
|
+
async dispose(instanceId) {
|
|
143
|
+
const instance = this.instances.get(instanceId);
|
|
144
|
+
if (!instance)
|
|
145
|
+
return;
|
|
146
|
+
instance.state = 'disposed';
|
|
147
|
+
// 关闭 per-instance MCP server
|
|
148
|
+
if (instance.mcpServer) {
|
|
149
|
+
await instance.mcpServer.close().catch((err) => {
|
|
150
|
+
this.logger.error(`[CCBroker] MCP server 关闭失败 (${instanceId}):`, err);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// 关闭 CC backend
|
|
154
|
+
await instance.backend.dispose().catch((err) => {
|
|
155
|
+
this.logger.error(`[CCBroker] backend dispose 失败 (${instanceId}):`, err);
|
|
156
|
+
});
|
|
157
|
+
this.instances.delete(instanceId);
|
|
158
|
+
this.logger.info(`[CCBroker] 实例 ${instanceId} 已释放`);
|
|
159
|
+
this.emit('instance.exited', instanceId, instance.role, instance.taskId);
|
|
160
|
+
// 检查 shutdown 等待
|
|
161
|
+
this.checkShutdownComplete();
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* 优雅关闭所有实例
|
|
165
|
+
*/
|
|
166
|
+
async shutdown(timeoutMs = 30_000) {
|
|
167
|
+
this.shuttingDown = true;
|
|
168
|
+
const activeInstances = [...this.instances.values()].filter(i => i.state !== 'disposed');
|
|
169
|
+
if (activeInstances.length === 0) {
|
|
170
|
+
return { unfinished: [] };
|
|
171
|
+
}
|
|
172
|
+
this.logger.info(`[CCBroker] 开始关闭 ${activeInstances.length} 个实例...`);
|
|
173
|
+
return new Promise((resolve) => {
|
|
174
|
+
const timer = setTimeout(() => {
|
|
175
|
+
// 超时:强制关闭剩余实例
|
|
176
|
+
const unfinished = [];
|
|
177
|
+
for (const instance of this.instances.values()) {
|
|
178
|
+
if (instance.state !== 'disposed') {
|
|
179
|
+
unfinished.push(instance.id);
|
|
180
|
+
instance.backend.dispose().catch(() => { });
|
|
181
|
+
if (instance.mcpServer) {
|
|
182
|
+
instance.mcpServer.close().catch(() => { });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
this.instances.clear();
|
|
187
|
+
resolve({ unfinished });
|
|
188
|
+
}, timeoutMs);
|
|
189
|
+
this.shutdownResolve = () => {
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
resolve({ unfinished: [] });
|
|
192
|
+
};
|
|
193
|
+
// 对所有实例发起 dispose
|
|
194
|
+
for (const instance of activeInstances) {
|
|
195
|
+
this.dispose(instance.id).catch(() => { });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* 释放所有实例(不进入 shutdown 模式,允许后续重新 spawn)
|
|
201
|
+
* 返回被释放的实例 ID 列表
|
|
202
|
+
*/
|
|
203
|
+
async disposeAll() {
|
|
204
|
+
const ids = [...this.instances.keys()];
|
|
205
|
+
if (ids.length === 0)
|
|
206
|
+
return [];
|
|
207
|
+
this.logger.info(`[CCBroker] 开始释放全部 ${ids.length} 个实例...`);
|
|
208
|
+
await Promise.all(ids.map(id => this.dispose(id).catch(() => { })));
|
|
209
|
+
this.logger.info('[CCBroker] 全部实例已释放');
|
|
210
|
+
return ids;
|
|
211
|
+
}
|
|
212
|
+
// ── 查询 ──
|
|
213
|
+
getInstance(id) {
|
|
214
|
+
return this.instances.get(id);
|
|
215
|
+
}
|
|
216
|
+
getInstancesByRole(role) {
|
|
217
|
+
return [...this.instances.values()].filter(i => i.role === role);
|
|
218
|
+
}
|
|
219
|
+
getInstanceByTaskId(taskId) {
|
|
220
|
+
return [...this.instances.values()].find(i => i.taskId === taskId);
|
|
221
|
+
}
|
|
222
|
+
status() {
|
|
223
|
+
return [...this.instances.values()].map(i => ({
|
|
224
|
+
id: i.id,
|
|
225
|
+
role: i.role,
|
|
226
|
+
state: i.state,
|
|
227
|
+
taskId: i.taskId,
|
|
228
|
+
cliSessionId: i.cliSessionId,
|
|
229
|
+
createdAt: i.createdAt.toISOString(),
|
|
230
|
+
lastActivityAt: i.lastActivityAt.toISOString(),
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
get instanceCount() {
|
|
234
|
+
return this.instances.size;
|
|
235
|
+
}
|
|
236
|
+
// ── 私有 ──
|
|
237
|
+
shutdownResolve = null;
|
|
238
|
+
countByRole(role) {
|
|
239
|
+
let count = 0;
|
|
240
|
+
for (const instance of this.instances.values()) {
|
|
241
|
+
if (instance.role === role && instance.state !== 'disposed') {
|
|
242
|
+
count++;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return count;
|
|
246
|
+
}
|
|
247
|
+
checkShutdownComplete() {
|
|
248
|
+
if (!this.shuttingDown || !this.shutdownResolve)
|
|
249
|
+
return;
|
|
250
|
+
const stillActive = [...this.instances.values()].some(i => i.state !== 'disposed');
|
|
251
|
+
if (!stillActive) {
|
|
252
|
+
this.shutdownResolve();
|
|
253
|
+
this.shutdownResolve = null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=cc-broker.js.map
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Anya CLI — Team Anya 服务启动/管理入口
|
|
4
|
+
*
|
|
5
|
+
* 用法:
|
|
6
|
+
* anya start [-p 3000] [--home ~/.anya] [-d]
|
|
7
|
+
* anya stop
|
|
8
|
+
* anya status
|
|
9
|
+
* anya setup
|
|
10
|
+
*/
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
import { resolve, dirname, join } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { fork } from 'node:child_process';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { writePid, readPid, removePid, isProcessAlive, healthCheck } from './daemon.js';
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
// 包根目录:从 apps/server/dist/ 往上 3 层到 monorepo 根
|
|
21
|
+
const PKG_ROOT = resolve(__dirname, '../../..');
|
|
22
|
+
function resolveHome(p) {
|
|
23
|
+
if (p.startsWith('~/') || p === '~')
|
|
24
|
+
return join(homedir(), p.slice(2));
|
|
25
|
+
return p;
|
|
26
|
+
}
|
|
27
|
+
/** 读取版本号 */
|
|
28
|
+
function getVersion() {
|
|
29
|
+
try {
|
|
30
|
+
const pkgPath = join(PKG_ROOT, 'package.json');
|
|
31
|
+
if (existsSync(pkgPath)) {
|
|
32
|
+
return JSON.parse(readFileSync(pkgPath, 'utf-8')).version ?? '0.1.0';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch { /* ignore */ }
|
|
36
|
+
return '0.1.0';
|
|
37
|
+
}
|
|
38
|
+
/** 设置公共环境变量(.env 先加载,CLI 显式传参再覆盖) */
|
|
39
|
+
function setEnvFromOpts(opts, portExplicit) {
|
|
40
|
+
const anyaHome = resolveHome(opts.home ?? process.env.ANYA_HOME ?? '~/.anya');
|
|
41
|
+
process.env.ANYA_HOME = anyaHome;
|
|
42
|
+
// 先加载 .env(不覆盖已有的环境变量,如系统级 env)
|
|
43
|
+
const envFile = join(anyaHome, '.env');
|
|
44
|
+
if (existsSync(envFile)) {
|
|
45
|
+
const lines = readFileSync(envFile, 'utf-8').split('\n');
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
const trimmed = line.trim();
|
|
48
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
49
|
+
continue;
|
|
50
|
+
const eqIdx = trimmed.indexOf('=');
|
|
51
|
+
if (eqIdx === -1)
|
|
52
|
+
continue;
|
|
53
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
54
|
+
const value = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
|
|
55
|
+
if (!process.env[key]) {
|
|
56
|
+
process.env[key] = value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// CLI 显式传参覆盖 .env(只有用户真的传了 -p 才覆盖)
|
|
61
|
+
if (portExplicit) {
|
|
62
|
+
process.env.PORT = opts.port;
|
|
63
|
+
}
|
|
64
|
+
// 指向包内的模板和协议文件
|
|
65
|
+
if (!process.env.OFFICE_TEMPLATE_DIR) {
|
|
66
|
+
process.env.OFFICE_TEMPLATE_DIR = resolve(PKG_ROOT, 'workspace');
|
|
67
|
+
}
|
|
68
|
+
if (!process.env.LOID_PROTOCOLS_DIR) {
|
|
69
|
+
process.env.LOID_PROTOCOLS_DIR = resolve(PKG_ROOT, 'anya/prompts/protocols');
|
|
70
|
+
}
|
|
71
|
+
return anyaHome;
|
|
72
|
+
}
|
|
73
|
+
const program = new Command();
|
|
74
|
+
program
|
|
75
|
+
.name('anya')
|
|
76
|
+
.description('Team Anya - AI 数字员工系统')
|
|
77
|
+
.version(getVersion());
|
|
78
|
+
// ── start ──
|
|
79
|
+
program
|
|
80
|
+
.command('start')
|
|
81
|
+
.description('启动 Anya 服务(API + Web UI)')
|
|
82
|
+
.option('-p, --port <port>', '服务端口')
|
|
83
|
+
.option('--home <path>', 'ANYA_HOME 目录', '~/.anya')
|
|
84
|
+
.option('-d, --daemon', '后台运行')
|
|
85
|
+
.action(async (opts, cmd) => {
|
|
86
|
+
// 检测 -p 是否由用户显式传入(而非 commander 默认值)
|
|
87
|
+
const portExplicit = cmd.getOptionValueSource('port') === 'cli';
|
|
88
|
+
const anyaHome = setEnvFromOpts(opts, portExplicit);
|
|
89
|
+
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
|
|
90
|
+
// 检查是否已有进程在运行
|
|
91
|
+
const existingPid = readPid(anyaHome);
|
|
92
|
+
if (existingPid && isProcessAlive(existingPid)) {
|
|
93
|
+
console.error(`Anya 已在运行 (PID: ${existingPid}),请先执行 anya stop`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
if (opts.daemon) {
|
|
97
|
+
// 后台模式:fork 子进程
|
|
98
|
+
const child = fork(__filename, ['_start-server'], {
|
|
99
|
+
detached: true,
|
|
100
|
+
stdio: 'ignore',
|
|
101
|
+
env: { ...process.env },
|
|
102
|
+
});
|
|
103
|
+
child.unref();
|
|
104
|
+
writePid(anyaHome, child.pid);
|
|
105
|
+
const port = process.env.PORT || '3000';
|
|
106
|
+
console.log(`Anya 已在后台启动 (PID: ${child.pid})`);
|
|
107
|
+
console.log(` API: http://localhost:${port}/api/`);
|
|
108
|
+
console.log(` Web UI: http://localhost:${port}/`);
|
|
109
|
+
console.log(` 日志: ${join(anyaHome, 'data/logs/anya.log')}`);
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
// 前台模式
|
|
113
|
+
writePid(anyaHome, process.pid);
|
|
114
|
+
process.on('exit', () => removePid(anyaHome));
|
|
115
|
+
try {
|
|
116
|
+
const { startServer } = await import('./main.js');
|
|
117
|
+
const { config } = await startServer();
|
|
118
|
+
console.log(`\nAnya 服务已启动`);
|
|
119
|
+
console.log(` API: http://localhost:${config.PORT}/api/`);
|
|
120
|
+
console.log(` Web UI: http://localhost:${config.PORT}/`);
|
|
121
|
+
console.log(` Home: ${anyaHome}\n`);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
removePid(anyaHome);
|
|
125
|
+
console.error('启动失败:', err);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
// ── stop ──
|
|
130
|
+
program
|
|
131
|
+
.command('stop')
|
|
132
|
+
.description('停止 Anya 服务')
|
|
133
|
+
.option('--home <path>', 'ANYA_HOME 目录', '~/.anya')
|
|
134
|
+
.action(async (opts) => {
|
|
135
|
+
const anyaHome = resolveHome(opts.home ?? process.env.ANYA_HOME ?? '~/.anya');
|
|
136
|
+
const pid = readPid(anyaHome);
|
|
137
|
+
if (!pid) {
|
|
138
|
+
console.log('未找到运行中的 Anya 进程');
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
if (!isProcessAlive(pid)) {
|
|
142
|
+
console.log(`PID ${pid} 已不存在,清理 PID 文件`);
|
|
143
|
+
removePid(anyaHome);
|
|
144
|
+
process.exit(0);
|
|
145
|
+
}
|
|
146
|
+
console.log(`正在停止 Anya (PID: ${pid})...`);
|
|
147
|
+
process.kill(pid, 'SIGTERM');
|
|
148
|
+
// 等待进程退出(最多 30 秒)
|
|
149
|
+
const deadline = Date.now() + 30_000;
|
|
150
|
+
while (Date.now() < deadline && isProcessAlive(pid)) {
|
|
151
|
+
await new Promise(r => setTimeout(r, 500));
|
|
152
|
+
}
|
|
153
|
+
if (isProcessAlive(pid)) {
|
|
154
|
+
console.log('进程未响应 SIGTERM,发送 SIGKILL...');
|
|
155
|
+
process.kill(pid, 'SIGKILL');
|
|
156
|
+
}
|
|
157
|
+
removePid(anyaHome);
|
|
158
|
+
console.log('Anya 已停止');
|
|
159
|
+
});
|
|
160
|
+
// ── status ──
|
|
161
|
+
program
|
|
162
|
+
.command('status')
|
|
163
|
+
.description('查看 Anya 运行状态')
|
|
164
|
+
.option('-p, --port <port>', '服务端口')
|
|
165
|
+
.option('--home <path>', 'ANYA_HOME 目录', '~/.anya')
|
|
166
|
+
.action(async (opts, cmd) => {
|
|
167
|
+
const anyaHome = resolveHome(opts.home ?? process.env.ANYA_HOME ?? '~/.anya');
|
|
168
|
+
// 加载 .env 获取端口
|
|
169
|
+
const portExplicit = cmd.getOptionValueSource('port') === 'cli';
|
|
170
|
+
setEnvFromOpts(opts, portExplicit);
|
|
171
|
+
const port = parseInt(process.env.PORT || '3000', 10);
|
|
172
|
+
const pid = readPid(anyaHome);
|
|
173
|
+
console.log(`ANYA_HOME: ${anyaHome}`);
|
|
174
|
+
console.log(`PORT: ${port}`);
|
|
175
|
+
if (!pid) {
|
|
176
|
+
console.log('状态: 未运行(无 PID 文件)');
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
const alive = isProcessAlive(pid);
|
|
180
|
+
console.log(`PID: ${pid} (${alive ? '运行中' : '已退出'})`);
|
|
181
|
+
if (!alive) {
|
|
182
|
+
removePid(anyaHome);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
const health = await healthCheck(port);
|
|
186
|
+
console.log(`HTTP: ${health.ok ? 'healthy' : 'unhealthy'} (status: ${health.status ?? 'N/A'})`);
|
|
187
|
+
process.exit(health.ok ? 0 : 1);
|
|
188
|
+
});
|
|
189
|
+
// ── setup ──
|
|
190
|
+
program
|
|
191
|
+
.command('setup')
|
|
192
|
+
.description('交互式初始化 Anya 配置')
|
|
193
|
+
.option('--home <path>', 'ANYA_HOME 目录', '~/.anya')
|
|
194
|
+
.action(async (opts) => {
|
|
195
|
+
const { createInterface } = await import('node:readline');
|
|
196
|
+
const { mkdirSync, writeFileSync, readFileSync, existsSync: exists } = await import('node:fs');
|
|
197
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
198
|
+
const ask = (q, fallback = '') => new Promise(resolve => {
|
|
199
|
+
const suffix = fallback ? ` (${fallback})` : '';
|
|
200
|
+
rl.question(`${q}${suffix}: `, ans => resolve(ans.trim() || fallback));
|
|
201
|
+
});
|
|
202
|
+
console.log('\n Anya Setup - 交互式配置向导\n');
|
|
203
|
+
// ── 1. ANYA_HOME ──
|
|
204
|
+
const anyaHome = resolveHome(await ask('数据目录 ANYA_HOME', opts.home ?? '~/.anya'));
|
|
205
|
+
// 创建目录结构
|
|
206
|
+
const dirs = ['data', 'data/logs', 'office', 'repos', 'media/feishu'];
|
|
207
|
+
for (const dir of dirs)
|
|
208
|
+
mkdirSync(join(anyaHome, dir), { recursive: true });
|
|
209
|
+
console.log(` -> 已创建 ${anyaHome}\n`);
|
|
210
|
+
// 读取已有配置
|
|
211
|
+
const envFile = join(anyaHome, '.env');
|
|
212
|
+
const existing = {};
|
|
213
|
+
if (exists(envFile)) {
|
|
214
|
+
for (const line of readFileSync(envFile, 'utf-8').split('\n')) {
|
|
215
|
+
const t = line.trim();
|
|
216
|
+
if (!t || t.startsWith('#'))
|
|
217
|
+
continue;
|
|
218
|
+
const eq = t.indexOf('=');
|
|
219
|
+
if (eq === -1)
|
|
220
|
+
continue;
|
|
221
|
+
existing[t.slice(0, eq).trim()] = t.slice(eq + 1).trim().replace(/^["']|["']$/g, '');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// ── 2. 基础配置 ──
|
|
225
|
+
console.log('-- 基础配置 --');
|
|
226
|
+
const port = await ask('服务端口 PORT', existing.PORT || '3000');
|
|
227
|
+
// ── 3. Claude Code(核心依赖)──
|
|
228
|
+
console.log('\n-- Claude Code(核心,Yor 执行引擎)--');
|
|
229
|
+
console.log(' Anya 通过 Claude Code CLI 驱动 AI Agent 执行任务');
|
|
230
|
+
console.log(' 需要先安装: npm install -g @anthropic-ai/claude-code\n');
|
|
231
|
+
const ccBinary = await ask('Claude Code 命令路径 CLAUDE_CODE_BINARY', existing.CLAUDE_CODE_BINARY || 'claude');
|
|
232
|
+
// ── 4. 飞书集成(可选)──
|
|
233
|
+
console.log('\n-- 飞书集成(可选,留空跳过)--');
|
|
234
|
+
console.log(' 配置后 Anya 可通过飞书群接收任务、推送进度');
|
|
235
|
+
console.log(' 需要在飞书开放平台创建应用并开启机器人能力\n');
|
|
236
|
+
const feishuAppId = await ask('飞书 App ID FEISHU_APP_ID', existing.FEISHU_APP_ID || '');
|
|
237
|
+
let feishuAppSecret = '';
|
|
238
|
+
if (feishuAppId) {
|
|
239
|
+
feishuAppSecret = await ask('飞书 App Secret FEISHU_APP_SECRET', existing.FEISHU_APP_SECRET || '');
|
|
240
|
+
}
|
|
241
|
+
rl.close();
|
|
242
|
+
// ── 生成 .env ──
|
|
243
|
+
const lines = [
|
|
244
|
+
'# Anya 配置文件(由 anya setup 生成)',
|
|
245
|
+
'',
|
|
246
|
+
'# 基础配置',
|
|
247
|
+
`PORT=${port}`,
|
|
248
|
+
'',
|
|
249
|
+
'# Claude Code',
|
|
250
|
+
`CLAUDE_CODE_BINARY=${ccBinary}`,
|
|
251
|
+
];
|
|
252
|
+
lines.push('');
|
|
253
|
+
if (feishuAppId) {
|
|
254
|
+
lines.push('# 飞书集成');
|
|
255
|
+
lines.push(`FEISHU_APP_ID=${feishuAppId}`);
|
|
256
|
+
lines.push(`FEISHU_APP_SECRET=${feishuAppSecret}`);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
lines.push('# 飞书集成(未配置)');
|
|
260
|
+
lines.push('# FEISHU_APP_ID=');
|
|
261
|
+
lines.push('# FEISHU_APP_SECRET=');
|
|
262
|
+
}
|
|
263
|
+
lines.push('');
|
|
264
|
+
writeFileSync(envFile, lines.join('\n'), 'utf-8');
|
|
265
|
+
console.log(`\n配置已写入: ${envFile}`);
|
|
266
|
+
// ── 环境检查 ──
|
|
267
|
+
console.log('\n-- 环境检查 --');
|
|
268
|
+
const { execSync: exec } = await import('node:child_process');
|
|
269
|
+
const checks = [
|
|
270
|
+
{ name: 'Node.js', cmd: 'node --version', required: true },
|
|
271
|
+
{ name: 'git', cmd: 'git --version', required: true },
|
|
272
|
+
{ name: 'Claude Code', cmd: `${ccBinary} --version`, required: true },
|
|
273
|
+
{ name: 'GitHub CLI (gh)', cmd: 'gh --version', required: false },
|
|
274
|
+
{ name: 'Python 3', cmd: 'python3 --version', required: false },
|
|
275
|
+
];
|
|
276
|
+
for (const c of checks) {
|
|
277
|
+
try {
|
|
278
|
+
const ver = exec(c.cmd, { encoding: 'utf-8', timeout: 5000 }).trim().split('\n')[0];
|
|
279
|
+
console.log(` [OK] ${c.name}: ${ver}`);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
const tag = c.required ? 'MISSING' : 'skip';
|
|
283
|
+
console.log(` [${tag}] ${c.name}: 未安装${c.required ? '(必需)' : '(可选)'}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
console.log('\n配置完成!运行 anya start 启动服务');
|
|
287
|
+
});
|
|
288
|
+
// ── 内部命令:daemon 子进程实际启动 ──
|
|
289
|
+
program
|
|
290
|
+
.command('_start-server', { hidden: true })
|
|
291
|
+
.action(async () => {
|
|
292
|
+
const { startServer } = await import('./main.js');
|
|
293
|
+
await startServer();
|
|
294
|
+
});
|
|
295
|
+
program.parse();
|
|
296
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { join, resolve, dirname } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
function resolveHome(p) {
|
|
7
|
+
if (p.startsWith('~/') || p === '~') {
|
|
8
|
+
return join(homedir(), p.slice(2));
|
|
9
|
+
}
|
|
10
|
+
return p;
|
|
11
|
+
}
|
|
12
|
+
const defaultAnyaHome = resolveHome('~/.anya');
|
|
13
|
+
/**
|
|
14
|
+
* 基于包自身位置推导默认路径(兼容 monorepo 开发 + npm 全局安装)
|
|
15
|
+
* - npm 包: config.js 在 apps/server/dist/,包根在 ../../..
|
|
16
|
+
* - monorepo 开发: config.ts 通过 tsx 运行,cwd 是项目根
|
|
17
|
+
*/
|
|
18
|
+
function findPkgRoot() {
|
|
19
|
+
// 从编译后的 dist/config.js 位置往上找
|
|
20
|
+
const fromDist = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
21
|
+
// 检查是否确实是包根(有 workspace/ 目录)
|
|
22
|
+
if (existsSync(join(fromDist, 'workspace')))
|
|
23
|
+
return fromDist;
|
|
24
|
+
// 开发模式 fallback:从 cwd 推导
|
|
25
|
+
if (existsSync(join(process.cwd(), 'workspace')))
|
|
26
|
+
return process.cwd();
|
|
27
|
+
return fromDist;
|
|
28
|
+
}
|
|
29
|
+
const PKG_ROOT = findPkgRoot();
|
|
30
|
+
const envSchema = z.object({
|
|
31
|
+
PORT: z.coerce.number().default(3000),
|
|
32
|
+
SQLITE_PATH: z.string().optional(),
|
|
33
|
+
ANYA_HOME: z.string().default(defaultAnyaHome).transform(resolveHome),
|
|
34
|
+
WORKSPACE_PATH: z.string().optional(),
|
|
35
|
+
MAX_YOR_CONCURRENCY: z.coerce.number().min(1).max(10).default(3),
|
|
36
|
+
CLAUDE_CODE_BINARY: z.string().default('claude'),
|
|
37
|
+
LOID_WORK_DIR: z.string().optional(),
|
|
38
|
+
LOID_PROTOCOLS_DIR: z.string().default(resolve(PKG_ROOT, 'anya/prompts/protocols')),
|
|
39
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
40
|
+
FEISHU_APP_ID: z.string().optional(),
|
|
41
|
+
FEISHU_APP_SECRET: z.string().optional(),
|
|
42
|
+
FEISHU_DAILY_REPORT_CHAT_ID: z.string().optional(),
|
|
43
|
+
PROJECTS_CONFIG_PATH: z.string().optional(),
|
|
44
|
+
REPOS_PATH: z.string().optional(),
|
|
45
|
+
LOG_DIR: z.string().optional(),
|
|
46
|
+
LOID_USE_CC: z.coerce.boolean().default(true),
|
|
47
|
+
OFFICE_TEMPLATE_DIR: z.string().default(resolve(PKG_ROOT, 'workspace')),
|
|
48
|
+
}).transform((val) => ({
|
|
49
|
+
...val,
|
|
50
|
+
SQLITE_PATH: val.SQLITE_PATH ?? join(val.ANYA_HOME, 'data', 'anya.db'),
|
|
51
|
+
WORKSPACE_PATH: val.WORKSPACE_PATH ?? join(val.ANYA_HOME, 'office'),
|
|
52
|
+
LOID_WORK_DIR: val.LOID_WORK_DIR ?? join(val.ANYA_HOME, 'office', 'loid'),
|
|
53
|
+
PROJECTS_CONFIG_PATH: val.PROJECTS_CONFIG_PATH ?? join(val.ANYA_HOME, 'office', 'projects.json'),
|
|
54
|
+
REPOS_PATH: val.REPOS_PATH ?? join(val.ANYA_HOME, 'repos'),
|
|
55
|
+
MEDIA_PATH: join(val.ANYA_HOME, 'media', 'feishu'),
|
|
56
|
+
LOG_DIR: val.LOG_DIR ?? join(val.ANYA_HOME, 'data', 'logs'),
|
|
57
|
+
}));
|
|
58
|
+
let _config = null;
|
|
59
|
+
export function loadConfig(env = process.env) {
|
|
60
|
+
const result = envSchema.safeParse(env);
|
|
61
|
+
if (!result.success) {
|
|
62
|
+
const formatted = result.error.issues
|
|
63
|
+
.map(i => ` ${i.path.join('.')}: ${i.message}`)
|
|
64
|
+
.join('\n');
|
|
65
|
+
throw new Error(`配置校验失败:\n${formatted}`);
|
|
66
|
+
}
|
|
67
|
+
_config = result.data;
|
|
68
|
+
return _config;
|
|
69
|
+
}
|
|
70
|
+
export function getConfig() {
|
|
71
|
+
if (!_config) {
|
|
72
|
+
throw new Error('配置未初始化,请先调用 loadConfig()');
|
|
73
|
+
}
|
|
74
|
+
return _config;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon 管理:PID 文件读写、进程检测、健康检查
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
function resolveHome(p) {
|
|
8
|
+
if (p.startsWith('~/') || p === '~')
|
|
9
|
+
return join(homedir(), p.slice(2));
|
|
10
|
+
return p;
|
|
11
|
+
}
|
|
12
|
+
function getPidFile(anyaHome) {
|
|
13
|
+
const dataDir = join(resolveHome(anyaHome), 'data');
|
|
14
|
+
mkdirSync(dataDir, { recursive: true });
|
|
15
|
+
return join(dataDir, 'anya.pid');
|
|
16
|
+
}
|
|
17
|
+
export function writePid(anyaHome, pid) {
|
|
18
|
+
writeFileSync(getPidFile(anyaHome), String(pid), 'utf-8');
|
|
19
|
+
}
|
|
20
|
+
export function readPid(anyaHome) {
|
|
21
|
+
const pidFile = getPidFile(anyaHome);
|
|
22
|
+
if (!existsSync(pidFile))
|
|
23
|
+
return null;
|
|
24
|
+
const raw = readFileSync(pidFile, 'utf-8').trim();
|
|
25
|
+
const pid = parseInt(raw, 10);
|
|
26
|
+
return Number.isNaN(pid) ? null : pid;
|
|
27
|
+
}
|
|
28
|
+
export function removePid(anyaHome) {
|
|
29
|
+
const pidFile = getPidFile(anyaHome);
|
|
30
|
+
if (existsSync(pidFile))
|
|
31
|
+
unlinkSync(pidFile);
|
|
32
|
+
}
|
|
33
|
+
export function isProcessAlive(pid) {
|
|
34
|
+
try {
|
|
35
|
+
process.kill(pid, 0);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function healthCheck(port) {
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(`http://localhost:${port}/api/dashboard`);
|
|
45
|
+
return { ok: res.ok, status: res.status };
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return { ok: false };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=daemon.js.map
|