opc-agent 4.1.1 → 4.1.2
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/USABILITY-ISSUES.md +73 -0
- package/dist/channels/web.js +8 -2
- package/dist/cli.js +200 -85
- package/dist/core/runtime.js +37 -15
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +105 -10
- package/dist/memory/deepbrain.d.ts +1 -1
- package/dist/memory/deepbrain.js +95 -4
- package/dist/scheduler/cron-engine.js +3 -36
- package/package.json +1 -1
- package/src/channels/web.ts +8 -2
- package/src/cli.ts +195 -92
- package/src/core/runtime.ts +25 -0
- package/src/doctor.ts +243 -156
- package/src/memory/deepbrain.ts +99 -5
- package/src/scheduler/cron-engine.ts +3 -3
package/src/doctor.ts
CHANGED
|
@@ -1,156 +1,243 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
3
|
-
import * as net from 'net';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
name: '
|
|
64
|
-
check: () => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import * as net from 'net';
|
|
4
|
+
import * as yaml from 'js-yaml';
|
|
5
|
+
|
|
6
|
+
export interface CheckResult {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
detail: string;
|
|
9
|
+
fix?: string;
|
|
10
|
+
optional?: boolean; // ⚠️ 而不是 ❌
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface DoctorCheck {
|
|
14
|
+
name: string;
|
|
15
|
+
check: () => CheckResult | Promise<CheckResult>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 读取 .env 文件并解析为 key-value */
|
|
19
|
+
function loadEnvFile(): Record<string, string> {
|
|
20
|
+
const envPath = '.env';
|
|
21
|
+
if (!existsSync(envPath)) return {};
|
|
22
|
+
const result: Record<string, string> = {};
|
|
23
|
+
try {
|
|
24
|
+
const content = readFileSync(envPath, 'utf-8');
|
|
25
|
+
for (const line of content.split('\n')) {
|
|
26
|
+
const trimmed = line.trim();
|
|
27
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
28
|
+
const eqIdx = trimmed.indexOf('=');
|
|
29
|
+
if (eqIdx === -1) continue;
|
|
30
|
+
result[trimmed.slice(0, eqIdx).trim()] = trimmed.slice(eqIdx + 1).trim();
|
|
31
|
+
}
|
|
32
|
+
} catch { /* ignore */ }
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 从 oad.yaml 读取 provider 配置 */
|
|
37
|
+
function loadOadProvider(): string | undefined {
|
|
38
|
+
for (const f of ['oad.yaml', 'agent.yaml']) {
|
|
39
|
+
if (existsSync(f)) {
|
|
40
|
+
try {
|
|
41
|
+
const cfg = yaml.load(readFileSync(f, 'utf-8')) as any;
|
|
42
|
+
return cfg?.spec?.provider?.default;
|
|
43
|
+
} catch { /* ignore */ }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getDoctorChecks(): DoctorCheck[] {
|
|
50
|
+
return [
|
|
51
|
+
{
|
|
52
|
+
name: 'Node.js version',
|
|
53
|
+
check: () => {
|
|
54
|
+
const v = process.versions.node.split('.').map(Number);
|
|
55
|
+
return {
|
|
56
|
+
ok: v[0] >= 18,
|
|
57
|
+
detail: `v${process.versions.node}`,
|
|
58
|
+
fix: v[0] < 18 ? 'Upgrade to Node 18+: https://nodejs.org' : undefined,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'npm version',
|
|
64
|
+
check: () => {
|
|
65
|
+
try {
|
|
66
|
+
const v = execSync('npm --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
67
|
+
return { ok: true, detail: `v${v}` };
|
|
68
|
+
} catch {
|
|
69
|
+
return { ok: false, detail: 'Not found', fix: 'Install npm: https://nodejs.org' };
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
// Ollama 是可选的(只有选了 ollama provider 才需要)
|
|
75
|
+
name: 'Ollama running',
|
|
76
|
+
check: async () => {
|
|
77
|
+
try {
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
80
|
+
const r = await fetch('http://localhost:11434/api/tags', { signal: controller.signal });
|
|
81
|
+
clearTimeout(timeout);
|
|
82
|
+
const data = await r.json() as any;
|
|
83
|
+
return { ok: true, detail: `${data.models?.length || 0} models available` };
|
|
84
|
+
} catch {
|
|
85
|
+
return { ok: false, detail: 'Not running', fix: 'Install Ollama: https://ollama.ai (optional, only needed for local models)', optional: true };
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
// 检查 oad.yaml 而不是 agent.yaml
|
|
91
|
+
name: 'oad.yaml exists',
|
|
92
|
+
check: () => {
|
|
93
|
+
const found = existsSync('./oad.yaml');
|
|
94
|
+
if (found) return { ok: true, detail: 'Found' };
|
|
95
|
+
// 检查是否有旧的 agent.yaml 需要迁移
|
|
96
|
+
if (existsSync('./agent.yaml')) {
|
|
97
|
+
return { ok: false, detail: 'Not found (found agent.yaml)', fix: 'Run `opc migrate` to migrate agent.yaml → oad.yaml' };
|
|
98
|
+
}
|
|
99
|
+
return { ok: false, detail: 'Not found', fix: 'Run `opc init` to create a project' };
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'SOUL.md exists',
|
|
104
|
+
check: () => {
|
|
105
|
+
const found = existsSync('./SOUL.md');
|
|
106
|
+
return { ok: found, detail: found ? 'Found' : 'Not found', fix: found ? undefined : 'Run `opc init` to generate one' };
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
// TypeScript 是可选的
|
|
111
|
+
name: 'TypeScript installed',
|
|
112
|
+
check: () => {
|
|
113
|
+
try {
|
|
114
|
+
execSync('npx tsc --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
115
|
+
return { ok: true, detail: 'Available' };
|
|
116
|
+
} catch {
|
|
117
|
+
return { ok: false, detail: 'Not found', fix: 'npm install -D typescript (optional)', optional: true };
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'Disk space',
|
|
123
|
+
check: () => {
|
|
124
|
+
return { ok: true, detail: 'Check passed' };
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
// DeepBrain 是可选的
|
|
129
|
+
name: 'DeepBrain package',
|
|
130
|
+
check: () => {
|
|
131
|
+
try {
|
|
132
|
+
require.resolve('deepbrain');
|
|
133
|
+
return { ok: true, detail: 'Installed' };
|
|
134
|
+
} catch {
|
|
135
|
+
return { ok: false, detail: 'Not installed', fix: 'npm install deepbrain (optional, for long-term memory)', optional: true };
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'Port 3000 available',
|
|
141
|
+
check: () => {
|
|
142
|
+
return new Promise<CheckResult>((resolve) => {
|
|
143
|
+
const server = net.createServer();
|
|
144
|
+
server.once('error', () => {
|
|
145
|
+
resolve({ ok: false, detail: 'In use', fix: 'Free port 3000 or configure a different port' });
|
|
146
|
+
});
|
|
147
|
+
server.once('listening', () => {
|
|
148
|
+
server.close(() => {
|
|
149
|
+
resolve({ ok: true, detail: 'Available' });
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
server.listen(3000);
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
// 检查 API key 是否配置(不是占位符)
|
|
158
|
+
name: 'API key configured',
|
|
159
|
+
check: () => {
|
|
160
|
+
const env = loadEnvFile();
|
|
161
|
+
const apiKey = env['OPC_LLM_API_KEY'] || '';
|
|
162
|
+
const oadProvider = loadOadProvider();
|
|
163
|
+
// Ollama 不需要 API key
|
|
164
|
+
if (oadProvider === 'ollama') {
|
|
165
|
+
return { ok: true, detail: 'Not required (Ollama provider)' };
|
|
166
|
+
}
|
|
167
|
+
if (!apiKey || apiKey === 'your-api-key-here') {
|
|
168
|
+
return { ok: false, detail: 'Not configured or still placeholder', fix: 'Edit .env and set OPC_LLM_API_KEY to your actual API key' };
|
|
169
|
+
}
|
|
170
|
+
return { ok: true, detail: 'Configured' };
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
// 检查 .env 和 oad.yaml 的 provider 是否匹配
|
|
175
|
+
name: 'Provider consistency',
|
|
176
|
+
check: () => {
|
|
177
|
+
const env = loadEnvFile();
|
|
178
|
+
const baseUrl = env['OPC_LLM_BASE_URL'] || '';
|
|
179
|
+
const oadProvider = loadOadProvider();
|
|
180
|
+
if (!oadProvider || !baseUrl) {
|
|
181
|
+
return { ok: true, detail: 'N/A (no config to compare)' };
|
|
182
|
+
}
|
|
183
|
+
// 检测 .env 的 base URL 暗示的 provider
|
|
184
|
+
let envProvider = 'unknown';
|
|
185
|
+
if (baseUrl.includes('openai.com')) envProvider = 'openai';
|
|
186
|
+
else if (baseUrl.includes('deepseek.com')) envProvider = 'deepseek';
|
|
187
|
+
else if (baseUrl.includes('localhost:11434')) envProvider = 'ollama';
|
|
188
|
+
else if (baseUrl.includes('anthropic.com')) envProvider = 'anthropic';
|
|
189
|
+
else if (baseUrl.includes('dashscope.aliyuncs.com')) envProvider = 'qwen';
|
|
190
|
+
|
|
191
|
+
if (envProvider === 'unknown') return { ok: true, detail: `Custom base URL (${oadProvider})` };
|
|
192
|
+
if (envProvider !== oadProvider && oadProvider !== 'auto') {
|
|
193
|
+
return { ok: false, detail: `Mismatch: .env → ${envProvider}, oad.yaml → ${oadProvider}`, fix: 'Update .env or oad.yaml to use the same provider' };
|
|
194
|
+
}
|
|
195
|
+
return { ok: true, detail: `Matched: ${oadProvider}` };
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function runDoctor(): Promise<{ passed: number; total: number }> {
|
|
202
|
+
const checks = getDoctorChecks();
|
|
203
|
+
const color = {
|
|
204
|
+
green: (s: string) => `\x1b[32m${s}\x1b[0m`,
|
|
205
|
+
red: (s: string) => `\x1b[31m${s}\x1b[0m`,
|
|
206
|
+
yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
|
|
207
|
+
dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
|
|
208
|
+
bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
console.log(`\n🔍 ${color.bold('OPC Agent Doctor')}\n`);
|
|
212
|
+
|
|
213
|
+
let passed = 0;
|
|
214
|
+
const total = checks.length;
|
|
215
|
+
|
|
216
|
+
for (const check of checks) {
|
|
217
|
+
try {
|
|
218
|
+
const result = await check.check();
|
|
219
|
+
// optional 项失败显示 ⚠️ 而不是 ❌
|
|
220
|
+
const icon = result.ok ? color.green('✅') : (result.optional ? color.yellow('⚠️') : color.red('❌'));
|
|
221
|
+
const name = check.name.padEnd(24);
|
|
222
|
+
console.log(` ${icon} ${name} ${result.detail}`);
|
|
223
|
+
if (!result.ok && result.fix) {
|
|
224
|
+
console.log(` → ${result.fix}`);
|
|
225
|
+
}
|
|
226
|
+
// optional 项即使失败也算 passed
|
|
227
|
+
if (result.ok || result.optional) passed++;
|
|
228
|
+
} catch (err) {
|
|
229
|
+
const name = check.name.padEnd(24);
|
|
230
|
+
console.log(` ${color.red('❌')} ${name} Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log(`\n Result: ${passed}/${total} checks passed`);
|
|
235
|
+
if (passed < total) {
|
|
236
|
+
console.log(`\n Fix the issues above to get the best experience.`);
|
|
237
|
+
} else {
|
|
238
|
+
console.log(`\n ${color.green('All checks passed!')} You're good to go.`);
|
|
239
|
+
}
|
|
240
|
+
console.log();
|
|
241
|
+
|
|
242
|
+
return { passed, total };
|
|
243
|
+
}
|
package/src/memory/deepbrain.ts
CHANGED
|
@@ -1,9 +1,103 @@
|
|
|
1
1
|
import type { Message, MemoryStore } from '../core/types';
|
|
2
2
|
import { InMemoryStore } from './index';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
4
|
+
import { join, resolve } from 'path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 本地 JSON 文件持久化存储,作为 DeepBrain 不可用时的 fallback。
|
|
8
|
+
* 数据保存在 .opc/memory.json,进程重启后记忆不会丢失。
|
|
9
|
+
*/
|
|
10
|
+
class FileBackedStore implements MemoryStore {
|
|
11
|
+
private store: Map<string, unknown> = new Map();
|
|
12
|
+
private conversations: Map<string, Message[]> = new Map();
|
|
13
|
+
private filePath: string;
|
|
14
|
+
private dirty = false;
|
|
15
|
+
private saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
16
|
+
|
|
17
|
+
constructor(baseDir: string = '.') {
|
|
18
|
+
const opcDir = join(resolve(baseDir), '.opc');
|
|
19
|
+
if (!existsSync(opcDir)) mkdirSync(opcDir, { recursive: true });
|
|
20
|
+
this.filePath = join(opcDir, 'memory.json');
|
|
21
|
+
this.loadFromFile();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private loadFromFile(): void {
|
|
25
|
+
if (!existsSync(this.filePath)) return;
|
|
26
|
+
try {
|
|
27
|
+
const data = JSON.parse(readFileSync(this.filePath, 'utf-8'));
|
|
28
|
+
if (data.store) {
|
|
29
|
+
for (const [k, v] of Object.entries(data.store)) {
|
|
30
|
+
this.store.set(k, v);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (data.conversations) {
|
|
34
|
+
for (const [k, v] of Object.entries(data.conversations)) {
|
|
35
|
+
this.conversations.set(k, v as Message[]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch { /* 文件损坏则忽略,从空开始 */ }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private scheduleSave(): void {
|
|
42
|
+
this.dirty = true;
|
|
43
|
+
if (this.saveTimer) return; // 已经有定时器在等了
|
|
44
|
+
// 延迟 1 秒批量写入,避免高频写磁盘
|
|
45
|
+
this.saveTimer = setTimeout(() => {
|
|
46
|
+
this.saveTimer = null;
|
|
47
|
+
if (this.dirty) this.saveToFile();
|
|
48
|
+
}, 1000);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private saveToFile(): void {
|
|
52
|
+
try {
|
|
53
|
+
const data = {
|
|
54
|
+
store: Object.fromEntries(this.store),
|
|
55
|
+
conversations: Object.fromEntries(this.conversations),
|
|
56
|
+
updatedAt: new Date().toISOString(),
|
|
57
|
+
};
|
|
58
|
+
writeFileSync(this.filePath, JSON.stringify(data, null, 2));
|
|
59
|
+
this.dirty = false;
|
|
60
|
+
} catch { /* 写入失败不影响运行 */ }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async get(key: string): Promise<unknown> {
|
|
64
|
+
return this.store.get(key);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async set(key: string, value: unknown): Promise<void> {
|
|
68
|
+
this.store.set(key, value);
|
|
69
|
+
this.scheduleSave();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async getConversation(sessionId: string): Promise<Message[]> {
|
|
73
|
+
return this.conversations.get(sessionId) ?? [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async addMessage(sessionId: string, message: Message): Promise<void> {
|
|
77
|
+
if (!this.conversations.has(sessionId)) {
|
|
78
|
+
this.conversations.set(sessionId, []);
|
|
79
|
+
}
|
|
80
|
+
const conv = this.conversations.get(sessionId)!;
|
|
81
|
+
conv.push(message);
|
|
82
|
+
// 每个 session 最多保留 200 条消息,避免文件无限增长
|
|
83
|
+
if (conv.length > 200) conv.splice(0, conv.length - 200);
|
|
84
|
+
this.scheduleSave();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async clear(sessionId?: string): Promise<void> {
|
|
88
|
+
if (sessionId) {
|
|
89
|
+
this.conversations.delete(sessionId);
|
|
90
|
+
} else {
|
|
91
|
+
this.store.clear();
|
|
92
|
+
this.conversations.clear();
|
|
93
|
+
}
|
|
94
|
+
this.scheduleSave();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
3
97
|
|
|
4
98
|
/**
|
|
5
99
|
* DeepBrain-backed memory store for long-term semantic memory.
|
|
6
|
-
* Falls back to
|
|
100
|
+
* Falls back to local JSON file storage (.opc/memory.json) if deepbrain package is not installed.
|
|
7
101
|
*/
|
|
8
102
|
export interface DeepBrainClient {
|
|
9
103
|
store(collection: string, id: string, content: string, metadata?: Record<string, unknown>): Promise<void>;
|
|
@@ -12,13 +106,13 @@ export interface DeepBrainClient {
|
|
|
12
106
|
}
|
|
13
107
|
|
|
14
108
|
export class DeepBrainMemoryStore implements MemoryStore {
|
|
15
|
-
private fallback:
|
|
109
|
+
private fallback: FileBackedStore;
|
|
16
110
|
private client: DeepBrainClient | null = null;
|
|
17
111
|
private collection: string;
|
|
18
112
|
private ready: Promise<boolean>;
|
|
19
113
|
|
|
20
114
|
constructor(options: { collection?: string; config?: Record<string, unknown> } = {}) {
|
|
21
|
-
this.fallback = new
|
|
115
|
+
this.fallback = new FileBackedStore();
|
|
22
116
|
this.collection = options.collection ?? 'agent-memory';
|
|
23
117
|
this.ready = this.initClient(options.config);
|
|
24
118
|
}
|
|
@@ -29,12 +123,12 @@ export class DeepBrainMemoryStore implements MemoryStore {
|
|
|
29
123
|
const deepbrain = await import(/* webpackIgnore: true */ 'deepbrain');
|
|
30
124
|
this.client = (deepbrain as any).createClient?.(config) ?? (deepbrain as any).default?.createClient?.(config);
|
|
31
125
|
if (!this.client) {
|
|
32
|
-
console.warn('[DeepBrainMemory] Could not create client, using
|
|
126
|
+
console.warn('[DeepBrainMemory] Could not create client, using file-backed fallback (.opc/memory.json)');
|
|
33
127
|
return false;
|
|
34
128
|
}
|
|
35
129
|
return true;
|
|
36
130
|
} catch {
|
|
37
|
-
console.warn('[DeepBrainMemory] deepbrain package not found, using
|
|
131
|
+
console.warn('[DeepBrainMemory] deepbrain package not found, using file-backed fallback (.opc/memory.json)');
|
|
38
132
|
return false;
|
|
39
133
|
}
|
|
40
134
|
}
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
8
|
import { join } from 'path';
|
|
9
|
-
import * as os from 'os';
|
|
10
9
|
import { parseCron, cronMatches, Scheduler } from '../core/scheduler';
|
|
11
10
|
import type { CronJob, JobHandler } from '../core/scheduler';
|
|
12
11
|
|
|
@@ -29,8 +28,9 @@ export interface SchedulesStore {
|
|
|
29
28
|
tasks: ScheduleTask[];
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
function getSchedulesPath(): string {
|
|
33
|
-
|
|
31
|
+
function getSchedulesPath(projectDir?: string): string {
|
|
32
|
+
// 使用项目本地路径而不是全局 ~/.opc/,避免新 agent 加载其他项目的任务
|
|
33
|
+
const dir = projectDir ? join(projectDir, '.opc') : join(process.cwd(), '.opc');
|
|
34
34
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
35
35
|
return join(dir, 'schedules.json');
|
|
36
36
|
}
|