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.
@@ -0,0 +1,73 @@
1
+ # OPC Agent 易用性问题清单
2
+
3
+ 日期: 2026-04-20
4
+ 目标: 易用性超过 OpenClaw 和 DeerFlow
5
+
6
+ ## 🔴 Critical (新用户第一次就遇到)
7
+
8
+ ### C1: .env 和 oad.yaml 配置矛盾
9
+ - .env 设了 `OPC_LLM_BASE_URL=https://api.openai.com/v1` 和 `OPC_LLM_MODEL=gpt-4o-mini`
10
+ - 但 oad.yaml 里 `provider: deepseek`, `model: deepseek-chat`
11
+ - 新用户困惑:到底听谁的?
12
+ - **修复**: init 时让用户选 provider,.env 和 oad.yaml 统一生成
13
+
14
+ ### C2: .env 占位符 API key 导致 401
15
+ - 默认 `OPC_LLM_API_KEY=your-api-key-here` 导致第一次对话直接 401
16
+ - 用户看到 "Incorrect API key" 错误,不知道怎么修
17
+ - **修复**: init 时提示输入 API key 或选择 Ollama(免费本地)
18
+ - 如果没配 key,chat 应该给友好提示而不是 raw 401 JSON
19
+
20
+ ### C3: agent.yaml vs oad.yaml 双配置文件
21
+ - init 生成了 agent.yaml 和 oad.yaml 两个文件
22
+ - doctor 检查 agent.yaml 但 runtime 读 oad.yaml
23
+ - **修复**: 统一用一个文件
24
+
25
+ ## 🟡 Important (影响体验但不致命)
26
+
27
+ ### I1: opc doctor 误报
28
+ - "agent.yaml exists: Not found" — 但实际有 oad.yaml
29
+ - "TypeScript installed: Not found" — 但 npx tsx 能跑
30
+ - "Ollama running: Not running" — 不是所有用户都用 Ollama
31
+ - "DeepBrain package: Not installed" — 但 opc-agent 自带内存 fallback
32
+ - **修复**: doctor 应该检查 oad.yaml,对非必须项给 warning 不给 ❌
33
+
34
+ ### I2: Chat 401 错误消息不友好
35
+ - 返回 raw SSE `data: {"error":"LLM API error 401: {...}"}`
36
+ - Web UI 应该显示 "请先配置 API Key" + 指引
37
+ - **修复**: 前端拦截 401,显示配置指引
38
+
39
+ ### I3: Studio /api/dashboard 404
40
+ - Dashboard API 不存在
41
+ - **修复**: 实现或移除 dashboard 入口
42
+
43
+ ### I4: DeepBrain fallback 无声失败
44
+ - 日志显示 `[DeepBrainMemory] deepbrain package not found, using in-memory fallback`
45
+ - 用户不知道记忆没有持久化
46
+ - **修复**: 首次运行时提示,或默认用文件存储
47
+
48
+ ### I5: 没有一键安装脚本
49
+ - OpenClaw 有 `curl | sh` 一键安装
50
+ - 我们需要 `npm install -g opc-agent`,还要自己装 Node
51
+ - **修复**: 做一个安装脚本,检测 OS、装 Node、装 opc-agent
52
+
53
+ ## 🟢 Nice-to-have
54
+
55
+ ### N1: 没有 `opc quickstart` 命令
56
+ - OpenClaw 五分钟上手,我们还是多步手动流程
57
+ - **修复**: 做一个交互式 quickstart,一步到位
58
+
59
+ ### N2: init 模板缺少 Ollama 选项
60
+ - 很多用户想用免费本地模型
61
+ - **修复**: init 时加 Ollama 选项,自动配 ollama provider
62
+
63
+ ### N3: README.md 内容单薄
64
+ - 缺少截图、GIF、快速上手步骤
65
+ - **修复**: 丰富 README
66
+
67
+ ### N4: Studio agent 数据来自旧缓存
68
+ - API 返回 "name: Updated Bot" 而不是 "my-first-agent"
69
+ - **修复**: 确保从 oad.yaml 实时读取
70
+
71
+ ### N5: cron-engine 默认 9 个任务
72
+ - 新建的 agent 不应该有预设的 cron 任务
73
+ - **修复**: 新 agent 默认 0 个 cron 任务
@@ -211,7 +211,7 @@ async function send(){
211
211
  for(const line of lines){
212
212
  if(!line.startsWith('data: '))continue;
213
213
  const dd=line.slice(6);if(dd==='[DONE]')continue;
214
- try{const j=JSON.parse(dd);if(j.content)full+=j.content;if(j.error)full='Error: '+j.error;}catch{}
214
+ try{const j=JSON.parse(dd);if(j.content)full+=j.content;if(j.error){if(j.error.includes('401')||j.error.toLowerCase().includes('api key')||j.error.toLowerCase().includes('unauthorized')){full='⚠️ 请先配置 API Key。编辑项目根目录的 .env 文件,设置 OPC_LLM_API_KEY。';}else{full='Error: '+j.error;}}}catch{}
215
215
  }
216
216
  d.innerHTML=renderMd(full)+'<span class="cursor"></span>';
217
217
  msgs.scrollTop=msgs.scrollHeight;
@@ -220,7 +220,13 @@ async function send(){
220
220
  const rx=document.createElement('div');rx.className='reactions';
221
221
  rx.innerHTML='<button data-r="👍" onclick="react(this)">👍</button><button data-r="👎" onclick="react(this)">👎</button>';
222
222
  wrap.appendChild(rx);
223
- }catch(e){d.className='msg error';d.textContent='Error: '+e.message;}
223
+ }catch(e){
224
+ d.className='msg error';
225
+ const errMsg=e.message||'';
226
+ if(errMsg.includes('401')||errMsg.includes('HTTP 401')){
227
+ d.innerHTML='⚠️ <b>请先配置 API Key</b><br>编辑项目根目录的 <code>.env</code> 文件,设置 <code>OPC_LLM_API_KEY</code> 为你的实际 API Key。';
228
+ }else{d.textContent='Error: '+errMsg;}
229
+ }
224
230
  sending=false;btn.disabled=false;input.focus();
225
231
  }
226
232
 
package/dist/cli.js CHANGED
@@ -124,7 +124,7 @@ async function select(question, options) {
124
124
  program
125
125
  .name('opc')
126
126
  .description('OPC Agent - Open Agent Framework for business workstations')
127
- .version('2.0.0');
127
+ .version(require('../package.json').version);
128
128
  // ── Init command ─────────────────────────────────────────────
129
129
  program
130
130
  .command('init')
@@ -186,7 +186,7 @@ program
186
186
  }
187
187
  const roleDisplayName = roleMeta.name || matched.role;
188
188
  const roleDescription = roleMeta.name_zh ? `${roleMeta.name} (${roleMeta.name_zh})` : (roleMeta.name || matched.role);
189
- console.log(` ${icon.info} Matched role: ${color.cyan(matched.category + '/' + matched.role)} ${roleDisplayName}`);
189
+ console.log(` ${icon.info} Matched role: ${color.cyan(matched.category + '/' + matched.role)} - ${roleDisplayName}`);
190
190
  // Create directories
191
191
  fs.mkdirSync(dir, { recursive: true });
192
192
  fs.mkdirSync(path.join(dir, 'src', 'skills'), { recursive: true });
@@ -205,9 +205,9 @@ program
205
205
  // Company-specific knowledge belongs to Desk (closed-source), not here.
206
206
  const workstationSeedFromRole = workstationMatch?.[0]?.trim() || '';
207
207
  fs.writeFileSync(path.join(dir, 'brain-seeds', 'workstation.md'), workstationSeedFromRole || `# Workstation Knowledge\n\n## Tools & Environment\n\nCommon tools and setup for this workstation role.\n\n## Workflows\n\nStandard operating procedures and workflows.\n\n## Best Practices\n\nIndustry best practices for this role.\n`);
208
- // agent.yaml with role system prompt and brain seeds
208
+ // oad.yaml with role system prompt and brain seeds(不再生成 agent.yaml)
209
209
  const firstLine = systemPromptContent.split('\n').find((l) => l.trim() && !l.startsWith('#'))?.trim() || 'You are a helpful AI assistant.';
210
- fs.writeFileSync(path.join(dir, 'agent.yaml'), `apiVersion: opc/v1
210
+ fs.writeFileSync(path.join(dir, 'oad.yaml'), `apiVersion: opc/v1
211
211
  kind: Agent
212
212
  metadata:
213
213
  name: ${name}
@@ -251,14 +251,14 @@ spec:
251
251
  if (roleData.files['oad.yaml']) {
252
252
  fs.writeFileSync(path.join(dir, 'oad.yaml'), roleData.files['oad.yaml']);
253
253
  }
254
- // src/index.ts entry point (same as generic)
254
+ // src/index.ts - entry point (same as generic)
255
255
  fs.writeFileSync(path.join(dir, 'src', 'index.ts'), `import { AgentRuntime } from 'opc-agent';
256
256
  import { EchoSkill } from './skills/echo';
257
257
  import { readFileSync, existsSync } from 'fs';
258
258
 
259
259
  async function main() {
260
260
  const runtime = new AgentRuntime();
261
- const config = await runtime.loadConfig('./agent.yaml');
261
+ const config = await runtime.loadConfig('./oad.yaml');
262
262
 
263
263
  const soul = existsSync('./SOUL.md') ? readFileSync('./SOUL.md', 'utf-8') : '';
264
264
  const context = existsSync('./CONTEXT.md') ? readFileSync('./CONTEXT.md', 'utf-8') : '';
@@ -305,15 +305,15 @@ export class EchoSkill extends BaseSkill {
305
305
  fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name, version: '1.0.0', private: true, scripts: { start: 'opc run', dev: 'opc dev', chat: 'opc chat', build: 'tsc' }, dependencies: { 'opc-agent': '^1.3.0' }, devDependencies: { typescript: '^5.5.0', tsx: '^4.0.0' } }, null, 2));
306
306
  // .gitignore, .env.example, .env
307
307
  fs.writeFileSync(path.join(dir, '.gitignore'), 'node_modules\ndist\n.env\n.opc-knowledge.json\ndata/\n');
308
- fs.writeFileSync(path.join(dir, '.env.example'), `# LLM API Configuration\nOPC_LLM_API_KEY=your-api-key-here\nOPC_LLM_BASE_URL=https://api.openai.com/v1\nOPC_LLM_MODEL=gpt-4o-mini\n`);
309
- fs.writeFileSync(path.join(dir, '.env'), `OPC_LLM_API_KEY=your-api-key-here\nOPC_LLM_BASE_URL=https://api.openai.com/v1\nOPC_LLM_MODEL=gpt-4o-mini\n`);
308
+ fs.writeFileSync(path.join(dir, '.env.example'), `# LLM API Configuration\n# Ollama (免费本地,默认,无需 API key):\nOPC_LLM_BASE_URL=http://localhost:11434/v1\nOPC_LLM_MODEL=qwen2.5\n\n# 如需使用商业模型,取消以下注释:\n# OPC_LLM_API_KEY=your-api-key-here\n# OPC_LLM_BASE_URL=https://api.deepseek.com/v1\n# OPC_LLM_MODEL=deepseek-chat\n`);
309
+ fs.writeFileSync(path.join(dir, '.env'), `# Ollama (免费本地) - 无需 API key\nOPC_LLM_BASE_URL=http://localhost:11434/v1\nOPC_LLM_MODEL=qwen2.5\n`);
310
310
  // README.md
311
311
  fs.writeFileSync(path.join(dir, 'README.md'), `# ${name}\n\nCreated with [OPC Agent](https://github.com/Deepleaper/opc-agent) using the \`${matched.category}/${matched.role}\` workstation role.\n\n## Quick Start\n\n\`\`\`bash\nnpm install\nollama pull qwen2.5\nnpx tsx src/index.ts\n\`\`\`\n\nOpen [http://localhost:3000](http://localhost:3000)\n`);
312
312
  // Dockerfile + docker-compose
313
- fs.writeFileSync(path.join(dir, 'Dockerfile'), `FROM node:22-alpine\nWORKDIR /app\nCOPY package.json package-lock.json* ./\nRUN npm ci --production 2>/dev/null || npm install --production\nCOPY oad.yaml agent.yaml .env* ./\nCOPY src/ ./src/\nCOPY prompts/ ./prompts/ 2>/dev/null || true\nEXPOSE 3000\nCMD ["npx", "opc", "run"]\n`);
314
- fs.writeFileSync(path.join(dir, 'docker-compose.yml'), `version: '3.8'\nservices:\n agent:\n build: .\n ports:\n - "3000:3000"\n env_file:\n - .env\n volumes:\n - ./agent.yaml:/app/agent.yaml:ro\n restart: unless-stopped\n`);
313
+ fs.writeFileSync(path.join(dir, 'Dockerfile'), `FROM node:22-alpine\nWORKDIR /app\nCOPY package.json package-lock.json* ./\nRUN npm ci --production 2>/dev/null || npm install --production\nCOPY oad.yaml .env* ./\nCOPY src/ ./src/\nCOPY prompts/ ./prompts/ 2>/dev/null || true\nEXPOSE 3000\nCMD ["npx", "opc", "run"]\n`);
314
+ fs.writeFileSync(path.join(dir, 'docker-compose.yml'), `version: '3.8'\nservices:\n agent:\n build: .\n ports:\n - "3000:3000"\n env_file:\n - .env\n volumes:\n - ./oad.yaml:/app/oad.yaml:ro\n restart: unless-stopped\n`);
315
315
  console.log(`\n${icon.success} Created agent project: ${color.bold(name + '/')} from role ${color.cyan(matched.category + '/' + matched.role)}`);
316
- console.log(` ${icon.file} agent.yaml - Agent definition with role system prompt`);
316
+ console.log(` ${icon.file} oad.yaml - Agent definition with role system prompt`);
317
317
  console.log(` ${icon.file} SOUL.md - Role personality (${systemPromptContent.split('\n').length} lines)`);
318
318
  console.log(` ${icon.file} CONTEXT.md - Role context & documentation`);
319
319
  console.log(` ${icon.file} brain-seeds/ - 3-tier brain seed knowledge`);
@@ -344,7 +344,7 @@ export class EchoSkill extends BaseSkill {
344
344
  }
345
345
  }
346
346
  catch {
347
- // Hub unreachable fall back to bundled templates
347
+ // Hub unreachable - fall back to bundled templates
348
348
  }
349
349
  let template;
350
350
  let selectedHubTemplate;
@@ -364,6 +364,116 @@ export class EchoSkill extends BaseSkill {
364
364
  else {
365
365
  template = await select('Select a template:', Object.entries(TEMPLATES).map(([value, { label }]) => ({ value, label })));
366
366
  }
367
+ // ── LLM Provider 选择(Ollama-first)──
368
+ let llmProvider = 'ollama';
369
+ let llmModel = 'qwen2.5';
370
+ let llmBaseUrl = 'http://localhost:11434/v1';
371
+ let llmApiKey = '';
372
+ let ollamaRunning = false;
373
+ let modelNames = [];
374
+ // 无论 --yes 还是交互式,都先检测 Ollama
375
+ try {
376
+ const controller = new AbortController();
377
+ const ollamaTimeout = setTimeout(() => controller.abort(), 3000);
378
+ const ollamaRes = await fetch('http://localhost:11434/api/tags', { signal: controller.signal });
379
+ clearTimeout(ollamaTimeout);
380
+ const ollamaData = await ollamaRes.json();
381
+ modelNames = (ollamaData.models || []).map((m) => m.name || m.model);
382
+ ollamaRunning = true;
383
+ if (opts.yes && modelNames.length > 0) {
384
+ // --yes 模式:自动选第一个已有模型
385
+ llmModel = modelNames[0];
386
+ }
387
+ }
388
+ catch {
389
+ ollamaRunning = false;
390
+ }
391
+ if (!opts.yes) {
392
+ if (ollamaRunning) {
393
+ console.log(`\n ${icon.info} ${color.dim('正在检测 Ollama...')}`);
394
+ console.log(` ${icon.success} Ollama 已运行,发现 ${modelNames.length} 个模型`);
395
+ // 选择 provider
396
+ llmProvider = await select('选择 LLM 引擎:', [
397
+ { value: 'ollama', label: '🟢 Ollama (免费本地,推荐) - 已检测到运行中' },
398
+ { value: 'deepseek', label: '🔵 DeepSeek - 高性价比国产模型' },
399
+ { value: 'openai', label: '⚪ OpenAI (GPT-4o)' },
400
+ { value: 'anthropic', label: '🟣 Anthropic (Claude)' },
401
+ { value: 'custom', label: '⚙️ 自定义 (手动输入 Base URL)' },
402
+ ]);
403
+ if (llmProvider === 'ollama') {
404
+ // 选择本地模型
405
+ const defaultModel = modelNames.includes('qwen2.5') ? 'qwen2.5' : (modelNames.includes('llama3') ? 'llama3' : (modelNames[0] || 'qwen2.5'));
406
+ if (modelNames.length > 0) {
407
+ llmModel = await select('选择 Ollama 模型:', modelNames.map((m) => ({ value: m, label: m + (m === defaultModel ? ' (推荐)' : '') })));
408
+ }
409
+ else {
410
+ console.log(` ${color.yellow('⚠️')} 没有发现已下载的模型,将使用默认 qwen2.5`);
411
+ console.log(` 运行 ${color.cyan('ollama pull qwen2.5')} 下载模型`);
412
+ llmModel = 'qwen2.5';
413
+ }
414
+ }
415
+ }
416
+ else {
417
+ // Ollama not running
418
+ console.log(`\n ${icon.info} ${color.dim('正在检测 Ollama...')}`);
419
+ console.log(` ${color.yellow('⚠️')} Ollama 未运行或未安装`);
420
+ llmProvider = await select('选择 LLM 引擎:', [
421
+ { value: 'ollama', label: '🟢 Ollama (免费本地,推荐) - 需先安装: https://ollama.ai' },
422
+ { value: 'deepseek', label: '🔵 DeepSeek - 高性价比国产模型' },
423
+ { value: 'openai', label: '⚪ OpenAI (GPT-4o)' },
424
+ { value: 'anthropic', label: '🟣 Anthropic (Claude)' },
425
+ { value: 'custom', label: '⚙️ 自定义 (手动输入 Base URL)' },
426
+ ]);
427
+ if (llmProvider === 'ollama') {
428
+ console.log(`\n ${icon.info} Ollama 安装指南:`);
429
+ console.log(` 1. 访问 ${color.cyan('https://ollama.ai')} 下载并安装`);
430
+ console.log(` 2. 运行 ${color.cyan('ollama pull qwen2.5')} 下载推荐模型`);
431
+ console.log(` 3. 然后 ${color.cyan('opc run')} 即可开始对话\n`);
432
+ llmModel = 'qwen2.5';
433
+ }
434
+ }
435
+ // 商业模型需要 API key
436
+ if (llmProvider === 'deepseek') {
437
+ llmBaseUrl = 'https://api.deepseek.com/v1';
438
+ llmModel = 'deepseek-chat';
439
+ llmApiKey = await promptUser('输入 DeepSeek API Key (可稍后在 .env 中配置,直接回车跳过)');
440
+ if (!llmApiKey) {
441
+ console.log(` ${icon.info} 稍后在 ${color.cyan('.env')} 文件中设置 ${color.bold('OPC_LLM_API_KEY')}`);
442
+ }
443
+ }
444
+ else if (llmProvider === 'openai') {
445
+ llmBaseUrl = 'https://api.openai.com/v1';
446
+ llmModel = 'gpt-4o-mini';
447
+ llmApiKey = await promptUser('输入 OpenAI API Key (可稍后在 .env 中配置,直接回车跳过)');
448
+ if (!llmApiKey) {
449
+ console.log(` ${icon.info} 稍后在 ${color.cyan('.env')} 文件中设置 ${color.bold('OPC_LLM_API_KEY')}`);
450
+ }
451
+ }
452
+ else if (llmProvider === 'anthropic') {
453
+ llmBaseUrl = 'https://api.anthropic.com/v1';
454
+ llmModel = 'claude-sonnet-4-20250514';
455
+ llmApiKey = await promptUser('输入 Anthropic API Key (可稍后在 .env 中配置,直接回车跳过)');
456
+ if (!llmApiKey) {
457
+ console.log(` ${icon.info} 稍后在 ${color.cyan('.env')} 文件中设置 ${color.bold('OPC_LLM_API_KEY')}`);
458
+ }
459
+ }
460
+ else if (llmProvider === 'custom') {
461
+ llmBaseUrl = await promptUser('输入 Base URL', 'http://localhost:11434/v1');
462
+ llmModel = await promptUser('输入模型名称', 'qwen2.5');
463
+ llmApiKey = await promptUser('输入 API Key (可选,直接回车跳过)');
464
+ // 尝试推断 provider
465
+ if (llmBaseUrl.includes('deepseek.com'))
466
+ llmProvider = 'deepseek';
467
+ else if (llmBaseUrl.includes('openai.com'))
468
+ llmProvider = 'openai';
469
+ else if (llmBaseUrl.includes('anthropic.com'))
470
+ llmProvider = 'anthropic';
471
+ else if (llmBaseUrl.includes('localhost:11434'))
472
+ llmProvider = 'ollama';
473
+ else
474
+ llmProvider = 'openai'; // OpenAI-compatible fallback
475
+ }
476
+ }
367
477
  const dir = path.resolve(name);
368
478
  if (fs.existsSync(dir)) {
369
479
  console.error(`\n${icon.error} Directory ${color.bold(name)} already exists.`);
@@ -374,37 +484,16 @@ export class EchoSkill extends BaseSkill {
374
484
  const factory = TEMPLATES[template]?.factory ?? customer_service_1.createCustomerServiceConfig;
375
485
  const config = factory();
376
486
  config.metadata.name = name;
487
+ // 用用户选择的 provider 和 model 覆盖模板默认值
488
+ config.spec.model = llmModel;
489
+ config.spec.provider = { default: llmProvider };
377
490
  // Ensure web channel exists
378
491
  if (!config.spec.channels.some((c) => c.type === 'web')) {
379
492
  config.spec.channels.push({ type: 'web', port: 3000 });
380
493
  }
494
+ // 只生成 oad.yaml,不生成 agent.yaml
381
495
  fs.writeFileSync(path.join(dir, 'oad.yaml'), yaml.dump(config, { lineWidth: 120 }));
382
- // agent.yaml standalone OAD config for runtime usage
383
- fs.writeFileSync(path.join(dir, 'agent.yaml'), `apiVersion: opc/v1
384
- kind: Agent
385
- metadata:
386
- name: ${name}
387
- version: 1.0.0
388
- description: My AI Agent
389
- spec:
390
- model: qwen2.5
391
- provider:
392
- default: ollama
393
- systemPrompt: |
394
- You are a helpful AI assistant named ${name}.
395
- Be concise, helpful, and friendly.
396
- channels:
397
- - type: web
398
- port: 3000
399
- memory:
400
- shortTerm: true
401
- longTerm:
402
- provider: deepbrain
403
- skills:
404
- - name: echo
405
- description: Echo test skill
406
- `);
407
- // src/index.ts — entry point
496
+ // src/index.ts - entry point
408
497
  fs.writeFileSync(path.join(dir, 'src', 'index.ts'), `import { AgentRuntime } from 'opc-agent';
409
498
  import { EchoSkill } from './skills/echo';
410
499
  import { readFileSync, existsSync } from 'fs';
@@ -413,7 +502,7 @@ async function main() {
413
502
  const runtime = new AgentRuntime();
414
503
 
415
504
  // Load OAD config
416
- const config = await runtime.loadConfig('./agent.yaml');
505
+ const config = await runtime.loadConfig('./oad.yaml');
417
506
 
418
507
  // Load personality and context files
419
508
  const soul = existsSync('./SOUL.md') ? readFileSync('./SOUL.md', 'utf-8') : '';
@@ -439,7 +528,7 @@ async function main() {
439
528
 
440
529
  main().catch(console.error);
441
530
  `);
442
- // src/skills/echo.ts example skill
531
+ // src/skills/echo.ts - example skill
443
532
  fs.writeFileSync(path.join(dir, 'src', 'skills', 'echo.ts'), `import { BaseSkill } from 'opc-agent';
444
533
  import type { AgentContext, Message, SkillResult } from 'opc-agent';
445
534
 
@@ -477,23 +566,39 @@ export class EchoSkill extends BaseSkill {
477
566
  }, null, 2));
478
567
  // .env.example
479
568
  fs.writeFileSync(path.join(dir, '.env.example'), `# LLM API Configuration
480
- OPC_LLM_API_KEY=your-api-key-here
481
- OPC_LLM_BASE_URL=https://api.openai.com/v1
482
- OPC_LLM_MODEL=gpt-4o-mini
569
+ # Ollama (免费本地,默认):
570
+ # OPC_LLM_BASE_URL=http://localhost:11434/v1
571
+ # OPC_LLM_MODEL=qwen2.5
572
+ # (Ollama 无需 API key)
483
573
 
484
- # For DeepSeek:
574
+ # DeepSeek:
575
+ # OPC_LLM_API_KEY=your-deepseek-key
485
576
  # OPC_LLM_BASE_URL=https://api.deepseek.com/v1
486
577
  # OPC_LLM_MODEL=deepseek-chat
487
578
 
488
- # For local Ollama (default in agent.yaml):
489
- # OPC_LLM_BASE_URL=http://localhost:11434/v1
490
- # OPC_LLM_MODEL=qwen2.5
491
- `);
492
- // .env (copy of example)
493
- fs.writeFileSync(path.join(dir, '.env'), `OPC_LLM_API_KEY=your-api-key-here
494
- OPC_LLM_BASE_URL=https://api.openai.com/v1
495
- OPC_LLM_MODEL=gpt-4o-mini
579
+ # OpenAI:
580
+ # OPC_LLM_API_KEY=your-openai-key
581
+ # OPC_LLM_BASE_URL=https://api.openai.com/v1
582
+ # OPC_LLM_MODEL=gpt-4o-mini
583
+
584
+ # Anthropic:
585
+ # OPC_LLM_API_KEY=your-anthropic-key
586
+ # OPC_LLM_BASE_URL=https://api.anthropic.com/v1
587
+ # OPC_LLM_MODEL=claude-sonnet-4-20250514
496
588
  `);
589
+ // .env - 根据用户选择生成正确的配置
590
+ const envLines = [];
591
+ if (llmProvider === 'ollama') {
592
+ envLines.push('# Ollama (免费本地) - 无需 API key');
593
+ envLines.push(`OPC_LLM_BASE_URL=${llmBaseUrl}`);
594
+ envLines.push(`OPC_LLM_MODEL=${llmModel}`);
595
+ }
596
+ else {
597
+ envLines.push(`OPC_LLM_API_KEY=${llmApiKey || 'your-api-key-here'}`);
598
+ envLines.push(`OPC_LLM_BASE_URL=${llmBaseUrl}`);
599
+ envLines.push(`OPC_LLM_MODEL=${llmModel}`);
600
+ }
601
+ fs.writeFileSync(path.join(dir, '.env'), envLines.join('\n') + '\n');
497
602
  // package.json
498
603
  fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({
499
604
  name,
@@ -520,7 +625,7 @@ OPC_LLM_MODEL=gpt-4o-mini
520
625
  WORKDIR /app
521
626
  COPY package.json package-lock.json* ./
522
627
  RUN npm ci --production 2>/dev/null || npm install --production
523
- COPY oad.yaml agent.yaml .env* ./
628
+ COPY oad.yaml .env* ./
524
629
  COPY src/ ./src/
525
630
  COPY prompts/ ./prompts/ 2>/dev/null || true
526
631
  EXPOSE 3000
@@ -536,7 +641,7 @@ services:
536
641
  env_file:
537
642
  - .env
538
643
  volumes:
539
- - ./agent.yaml:/app/agent.yaml:ro
644
+ - ./oad.yaml:/app/oad.yaml:ro
540
645
  restart: unless-stopped
541
646
  `);
542
647
  // README.md
@@ -577,8 +682,7 @@ npx opc chat # CLI chat
577
682
 
578
683
  \`\`\`
579
684
  ${name}/
580
- ├── agent.yaml # OAD agent config (used by src/index.ts)
581
- ├── oad.yaml # OAD config (used by opc CLI)
685
+ ├── oad.yaml # Agent 配置 (唯一配置文件)
582
686
  ├── src/
583
687
  │ ├── index.ts # Entry point
584
688
  │ └── skills/
@@ -589,9 +693,9 @@ ${name}/
589
693
 
590
694
  ## Configuration
591
695
 
592
- Edit \`agent.yaml\` to customize your agent's personality, skills, and behavior.
696
+ Edit \`oad.yaml\` to customize your agent's personality, skills, and behavior.
593
697
  `);
594
- // SOUL.md agent personality
698
+ // SOUL.md - agent personality
595
699
  const createdDate = new Date().toISOString().split('T')[0];
596
700
  fs.writeFileSync(path.join(dir, 'SOUL.md'), `# ${name} Personality
597
701
 
@@ -607,7 +711,7 @@ Edit \`agent.yaml\` to customize your agent's personality, skills, and behavior.
607
711
 
608
712
  ## Communication Style
609
713
  - Use clear, simple language
610
- - Be direct answer the question first, then explain
714
+ - Be direct - answer the question first, then explain
611
715
  - Use markdown formatting when helpful
612
716
 
613
717
  ## Rules
@@ -615,7 +719,7 @@ Edit \`agent.yaml\` to customize your agent's personality, skills, and behavior.
615
719
  - Ask for clarification when the request is ambiguous
616
720
  - Never make up information
617
721
  `);
618
- // CONTEXT.md project context
722
+ // CONTEXT.md - project context
619
723
  fs.writeFileSync(path.join(dir, 'CONTEXT.md'), `# Project Context
620
724
 
621
725
  ## About This Agent
@@ -631,7 +735,8 @@ on startup to understand the project context.
631
735
  - Add company policies here
632
736
  `);
633
737
  console.log(`\n${icon.success} Created agent project: ${color.bold(name + '/')}`);
634
- console.log(` ${icon.file} agent.yaml - Agent definition (OAD)`);
738
+ console.log(` ${icon.file} oad.yaml - Agent 配置 (${llmProvider}/${llmModel})`);
739
+ console.log(` ${icon.file} .env - 环境变量${llmProvider === 'ollama' ? '' : ' (API Key)'}`);
635
740
  console.log(` ${icon.file} src/index.ts - Entry point`);
636
741
  console.log(` ${icon.file} src/skills/echo.ts - Example skill`);
637
742
  console.log(` ${icon.file} SOUL.md - Agent personality`);
@@ -656,14 +761,24 @@ on startup to understand the project context.
656
761
  }
657
762
  }
658
763
  catch {
659
- // Brain-seed download failed non-fatal, project still usable
764
+ // Brain-seed download failed - non-fatal, project still usable
660
765
  }
661
766
  }
662
767
  console.log(`\n${color.bold('Next steps:')}`);
663
768
  console.log(` 1. cd ${name}`);
664
769
  console.log(` 2. npm install`);
665
- console.log(` 3. npx tsx src/index.ts ${color.dim('# or: npx opc run')}`);
666
- console.log(` 4. Open http://localhost:3000\n`);
770
+ if (llmProvider === 'ollama' && !ollamaRunning) {
771
+ console.log(` 3. ollama pull ${llmModel} ${color.dim('# 下载模型')}`);
772
+ console.log(` 4. npx opc run ${color.dim('# 启动 Agent')}`);
773
+ }
774
+ else if (llmProvider !== 'ollama' && !llmApiKey) {
775
+ console.log(` 3. 编辑 .env 设置 OPC_LLM_API_KEY`);
776
+ console.log(` 4. npx opc run`);
777
+ }
778
+ else {
779
+ console.log(` 3. npx opc run ${color.dim('# 启动 Agent')}`);
780
+ }
781
+ console.log(` Open http://localhost:3000\n`);
667
782
  console.log(`${color.dim('💡 Tip: Use --role to start from a workstation template:')}`);
668
783
  console.log(`${color.dim(' opc init my-agent --role customer-service')}`);
669
784
  console.log(`${color.dim(' opc init --list-roles (see all roles)')}\n`);
@@ -713,7 +828,7 @@ program
713
828
  // Print startup banner
714
829
  const bannerLines = [
715
830
  '╔══════════════════════════════════════╗',
716
- '║ 🤖 OPC Agent Interactive Chat ║',
831
+ '║ 🤖 OPC Agent - Interactive Chat ║',
717
832
  `║ Agent: ${(agentName + ' v' + agentVersion).padEnd(27)}║`,
718
833
  `║ Model: ${((providerName + '/' + (model ?? 'default')).slice(0, 27)).padEnd(27)}║`,
719
834
  `║ Skills: ${(String(skillNames.length) + ' loaded').padEnd(26)}║`,
@@ -740,12 +855,12 @@ program
740
855
  }
741
856
  if (lower === '/help') {
742
857
  console.log(`\n ${color.bold('Available commands:')}`);
743
- console.log(` ${color.cyan('/help')} Show this help`);
744
- console.log(` ${color.cyan('/quit')} Exit chat (/exit also works)`);
745
- console.log(` ${color.cyan('/clear')} Clear conversation history`);
746
- console.log(` ${color.cyan('/skills')} List registered skills`);
747
- console.log(` ${color.cyan('/memory')} Show memory stats`);
748
- console.log(` ${color.cyan('/info')} Show agent info\n`);
858
+ console.log(` ${color.cyan('/help')} - Show this help`);
859
+ console.log(` ${color.cyan('/quit')} - Exit chat (/exit also works)`);
860
+ console.log(` ${color.cyan('/clear')} - Clear conversation history`);
861
+ console.log(` ${color.cyan('/skills')} - List registered skills`);
862
+ console.log(` ${color.cyan('/memory')} - Show memory stats`);
863
+ console.log(` ${color.cyan('/info')} - Show agent info\n`);
749
864
  return true;
750
865
  }
751
866
  if (lower === '/clear') {
@@ -1460,7 +1575,7 @@ protocolCmd.command('list')
1460
1575
  const protocols = config?.spec?.protocols || {};
1461
1576
  const items = [
1462
1577
  { name: 'a2a', description: 'Agent-to-Agent protocol', enabled: !!protocols.a2a?.enabled, detail: protocols.a2a?.port ? `port ${protocols.a2a.port}` : '' },
1463
- { name: 'agui', description: 'AG-UI Agent-User Interaction (SSE)', enabled: !!protocols.agui?.enabled, detail: protocols.agui?.path || '/agui' },
1578
+ { name: 'agui', description: 'AG-UI - Agent-User Interaction (SSE)', enabled: !!protocols.agui?.enabled, detail: protocols.agui?.path || '/agui' },
1464
1579
  ];
1465
1580
  console.log(`\n${icon.gear} ${color.bold('Protocols')}\n`);
1466
1581
  for (const p of items) {
@@ -1593,7 +1708,7 @@ brainCmd
1593
1708
  .description('Show brain stats (pages, tiers, last evolve)')
1594
1709
  .option('--url <url>', 'DeepBrain server URL', 'http://localhost:3333')
1595
1710
  .action(async (opts) => {
1596
- console.log(`\n${icon.gear} ${color.bold('DeepBrain Status')} ${color.dim(opts.url)}\n`);
1711
+ console.log(`\n${icon.gear} ${color.bold('DeepBrain Status')} - ${color.dim(opts.url)}\n`);
1597
1712
  try {
1598
1713
  const res = await fetch(`${opts.url}/api/stats`);
1599
1714
  if (!res.ok)
@@ -1626,7 +1741,7 @@ brainCmd
1626
1741
  brainCmd
1627
1742
  .command('seed')
1628
1743
  .description('Import brain seed files into memory')
1629
- .option('-f, --file <file>', 'OAD file', 'agent.yaml')
1744
+ .option('-f, --file <file>', 'OAD file', 'oad.yaml')
1630
1745
  .option('--status', 'Check if seeds have been imported')
1631
1746
  .option('--reset', 'Re-import seeds (clear marker and re-seed)')
1632
1747
  .action(async (opts) => {
@@ -1696,7 +1811,7 @@ brainCmd
1696
1811
  console.log(` ${color.cyan(c.slug)} → ${c.fromTier} → ${c.toTier} (confidence: ${(c.confidence * 100).toFixed(0)}%)`);
1697
1812
  }
1698
1813
  if (opts.dryRun) {
1699
- console.log(`\n ${icon.info} Dry run no changes made.\n`);
1814
+ console.log(`\n ${icon.info} Dry run - no changes made.\n`);
1700
1815
  }
1701
1816
  else {
1702
1817
  console.log(`\n ${icon.success} Promoted ${result.promoted} knowledge entries.\n`);
@@ -2145,7 +2260,7 @@ program
2145
2260
  return;
2146
2261
  }
2147
2262
  // Create a minimal mock agent for eval (real usage would load from OAD)
2148
- const oadPath = path.resolve('agent.yaml');
2263
+ const oadPath = path.resolve(fs.existsSync('oad.yaml') ? 'oad.yaml' : 'agent.yaml');
2149
2264
  let agent;
2150
2265
  if (fs.existsSync(oadPath)) {
2151
2266
  const runtime = new runtime_1.AgentRuntime();
@@ -2154,7 +2269,7 @@ program
2154
2269
  agent = runtime.agent;
2155
2270
  }
2156
2271
  if (!agent) {
2157
- console.log(`${icon.warn} No agent.yaml found running with dry-run mock agent.`);
2272
+ console.log(`${icon.warn} No oad.yaml or agent.yaml found - running with dry-run mock agent.`);
2158
2273
  agent = { chat: async (input) => `[mock response to: ${input}]` };
2159
2274
  }
2160
2275
  const evaluator = new eval_1.AgentEvaluator(agent);
@@ -2214,7 +2329,7 @@ guardrailsCmd
2214
2329
  console.log();
2215
2330
  const result = await manager.checkInput(message);
2216
2331
  if (result.passed) {
2217
- console.log(color.green('✓ PASSED no violations'));
2332
+ console.log(color.green('✓ PASSED - no violations'));
2218
2333
  }
2219
2334
  else {
2220
2335
  if (result.blocked)
@@ -2243,7 +2358,7 @@ program
2243
2358
  .action(async (opts) => {
2244
2359
  console.log(color.bold('🎤 Voice Conversation Mode'));
2245
2360
  console.log(` STT: ${opts.stt} | TTS: ${opts.tts} | Voice: ${opts.voice ?? 'default'} | Language: ${opts.language}`);
2246
- console.log(color.dim(' (Voice conversation requires audio input integration use as library)'));
2361
+ console.log(color.dim(' (Voice conversation requires audio input integration - use as library)'));
2247
2362
  console.log();
2248
2363
  console.log('To use voice in your agent:');
2249
2364
  console.log(color.cyan(`
@@ -2307,7 +2422,7 @@ keysCmd
2307
2422
  });
2308
2423
  // ── Approve command ───────────────────────────────────────────
2309
2424
  const approveCmd = program.command('approve').description('Manage command approvals');
2310
- // Singleton for CLI in real usage this would be loaded from daemon state
2425
+ // Singleton for CLI - in real usage this would be loaded from daemon state
2311
2426
  const approvalManager = new approval_1.ApprovalManager();
2312
2427
  approveCmd
2313
2428
  .command('list')
@@ -2439,7 +2554,7 @@ a2aCmd
2439
2554
  const { oadToAgentCard } = require('./protocols/a2a');
2440
2555
  const oad = loadOADFile();
2441
2556
  if (!oad) {
2442
- console.log(`${icon.error} No agent.yaml found`);
2557
+ console.log(`${icon.error} No oad.yaml or agent.yaml found`);
2443
2558
  return;
2444
2559
  }
2445
2560
  const card = oadToAgentCard(oad, 'http://localhost:3001');
@@ -2479,7 +2594,7 @@ a2aCmd
2479
2594
  function loadOADFile() {
2480
2595
  const fs = require('fs');
2481
2596
  const yaml = require('js-yaml');
2482
- for (const name of ['agent.yaml', 'agent.yml']) {
2597
+ for (const name of ['oad.yaml', 'agent.yaml', 'agent.yml']) {
2483
2598
  if (fs.existsSync(name)) {
2484
2599
  return yaml.load(fs.readFileSync(name, 'utf-8'));
2485
2600
  }
@@ -2487,7 +2602,7 @@ function loadOADFile() {
2487
2602
  return null;
2488
2603
  }
2489
2604
  // ── MCP Server Commands ────────────────────────────────────
2490
- const mcpCmd = program.command('mcp').description('MCP server commands expose agent as MCP tools');
2605
+ const mcpCmd = program.command('mcp').description('MCP server commands - expose agent as MCP tools');
2491
2606
  mcpCmd
2492
2607
  .command('serve')
2493
2608
  .option('--http <port>', 'Start HTTP+SSE mode on given port')
@@ -2516,7 +2631,7 @@ mcpCmd
2516
2631
  console.log(`${icon.info} Tools: ${server.getToolCount()}`);
2517
2632
  }
2518
2633
  else {
2519
- console.error(`${icon.success} MCP server (stdio) started ${server.getToolCount()} tools`);
2634
+ console.error(`${icon.success} MCP server (stdio) started - ${server.getToolCount()} tools`);
2520
2635
  await server.serveStdio();
2521
2636
  }
2522
2637
  });
@@ -2565,7 +2680,7 @@ mcpCmd
2565
2680
  console.log(`${icon.info} Tools: ${server.getToolCount()}`);
2566
2681
  }
2567
2682
  else {
2568
- console.error(`${icon.success} MCP server ${color.cyan(name)} (stdio) ${server.getToolCount()} tools`);
2683
+ console.error(`${icon.success} MCP server ${color.cyan(name)} (stdio) - ${server.getToolCount()} tools`);
2569
2684
  await server.serveStdio();
2570
2685
  }
2571
2686
  });