Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ FEISHU_TOOLS_CALENDAR=true
# 注入记忆最大 token 数 (默认 4000,约 20-30 条记忆)
# MEMORY_MAX_INJECT_TOKENS=4000

# === Onboarding ===
# 首次配置完成标记 (npm run onboard 完成后自动写入,无需手动配置)
# ONBOARDING_COMPLETED=true

# === Agent 配置 ===
# Agent 配置文件路径 (默认 ./config/agents.json)
# 首次使用: cp config/agents.example.json config/agents.json 然后自定义
Expand Down
9 changes: 5 additions & 4 deletions config/agents.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,26 @@
"maxBudgetUsd": 5,
"maxTurns": 100,
"requiresApproval": false,
"settingSources": ["user", "project"]
"settingSources": ["user", "project"],
"editablePathPatterns": ["config/personas/*", "config/knowledge/*"]
},
"agents": [
{
"id": "pm",
"displayName": "PM Bot",
"displayName": "<onboarding 中设定>",
"model": "claude-opus-4-6",
"replyMode": "direct",
"toolPolicy": {
"profile": "readonly",
"allow": ["Skill", "Bash"]
},
"bashAllowPatterns": ["^(ls|cat|head|tail|wc|find|grep|git (log|status|diff|show|branch))"],
"persona": "./personas/pm.md",
"persona": "./personas/assistant.md",
"knowledge": ["team.md"]
},
{
"id": "dev",
"displayName": "DevBot",
"displayName": "<onboarding 中设定>",
"model": "claude-opus-4-6",
"toolPolicy": "all",
"replyMode": "thread",
Expand Down
22 changes: 22 additions & 0 deletions config/personas/assistant.example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
你是一个 AI 助手,通过飞书与团队成员交互。

## 身份

- **名字**:(在 onboarding 中设定)
- **角色**:(在 onboarding 中设定,如:全栈开发助手、产品经理、技术顾问等)
- **性格**:(在 onboarding 中设定,如:简洁高效、幽默随和、技术范等)

## 能力

- 理解业务逻辑和用户场景
- 阅读和分析代码
- 协助梳理任务、回答技术问题

## 沟通风格

- 简洁直接,适合群聊节奏
- 给建议时明确表达判断
- 需求不清晰时主动追问

---
此文件是模板。运行 `npm run onboard` 后会根据你的设定生成正式的人设文件。
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"onboard": "tsx scripts/onboard.ts",
"build": "tsc",
"start": "node dist/index.js",
"lint": "eslint src/",
Expand Down
194 changes: 194 additions & 0 deletions scripts/onboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
#!/usr/bin/env tsx
/**
* CLI Onboarding Agent — 交互式配置助手
*
* 通过 Agent SDK 在终端中启动多轮对话,引导用户完成 Anycode 首次配置:
* - 飞书应用配置 (App ID/Secret, 权限, 事件)
* - 团队信息 (写入 config/knowledge/team.md)
* - Bot 人格设定 (写入 config/personas/pm.md)
* - 可选功能 (记忆系统、定时任务等)
*
* 用法: npm run onboard 或 npx tsx scripts/onboard.ts
*/
import 'dotenv/config';
import { query } from '@anthropic-ai/claude-agent-sdk';
import * as readline from 'node:readline/promises';
import { getBootstrapPrompt, isOnboardingCompleted } from '../src/onboarding/bootstrap.js';

// 颜色
const CYAN = '\x1b[36m';
const GREEN = '\x1b[32m';
const YELLOW = '\x1b[33m';
const BOLD = '\x1b[1m';
const DIM = '\x1b[2m';
const RESET = '\x1b[0m';

// Spinner — 等待 agent 回复时显示
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let spinnerTimer: ReturnType<typeof setInterval> | undefined;
function startSpinner(label = '思考中') {
let i = 0;
spinnerTimer = setInterval(() => {
process.stdout.write(`\r${DIM}${SPINNER_FRAMES[i % SPINNER_FRAMES.length]} ${label}...${RESET} `);
i++;
}, 80);
}
function stopSpinner() {
if (spinnerTimer) {
clearInterval(spinnerTimer);
spinnerTimer = undefined;
process.stdout.write('\r\x1b[K'); // 清除 spinner 行
}
}

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

// 检查是否已完成
if (isOnboardingCompleted()) {
console.log(`\n${GREEN}Onboarding 已完成。${RESET}`);
const answer = await rl.question(`${YELLOW}要重新配置吗?(y/N): ${RESET}`);
if (answer.trim().toLowerCase() !== 'y') {
console.log('退出。运行 npm run dev 启动服务。');
rl.close();
process.exit(0);
}
}

console.log(`\n${BOLD}${CYAN}-- Anycode 配置助手 --${RESET}\n`);
console.log(`${DIM}AI 助手将引导你完成飞书配置、团队信息和 Bot 人格设定。${RESET}`);
console.log(`${DIM}输入 "跳过" 跳过当前步骤,输入 "退出" 结束配置。${RESET}\n`);

const systemPrompt = getBootstrapPrompt();
let sessionId: string | undefined;
let userMessage = '开始配置。请先帮我配置飞书应用。';
let round = 0;
const MAX_ROUNDS = 30;

while (round < MAX_ROUNDS) {
round++;

try {
startSpinner(round === 1 ? '正在连接 AI 服务' : '思考中');
const q = query({
prompt: userMessage,
options: {
cwd: process.cwd(),
systemPrompt: systemPrompt,
permissionMode: 'acceptEdits',
model: process.env.CLAUDE_MODEL || 'claude-sonnet-4-6',
maxTurns: 50,
maxBudgetUsd: 5,
// 只传递必要的环境变量给 Claude Code 子进程
// 避免 .env 中的其他变量(如 OWNER_USER_ID)干扰 SDK 认证
env: (() => {
const e: Record<string, string> = {};
// 继承系统 PATH 等基础环境
for (const [k, v] of Object.entries(process.env)) {
if (v != null) e[k] = v;
}
// 确保清除可能干扰 Claude CLI 的嵌套检测变量
delete e.CLAUDECODE;
return e;
})(),
// 捕获子进程 stderr 输出(调试用)
stderr: (data: string) => {
const trimmed = data.trim();
if (trimmed) console.error(`${DIM}[stderr] ${trimmed}${RESET}`);
},
...(sessionId ? { resume: sessionId } : {}),
canUseTool: async (_toolName: string, inputObj: Record<string, unknown>) => {
return { behavior: 'allow' as const, updatedInput: inputObj };
},
},
});

let hasOutput = false;

for await (const msg of q) {
switch (msg.type) {
case 'system':
if (msg.subtype === 'init') {
sessionId = msg.session_id;
}
break;

case 'assistant':
stopSpinner();
if (msg.message?.content) {
for (const block of msg.message.content) {
if ('text' in block && block.text) {
// 过滤 thinking 标签
const text = block.text
.replace(/<thinking>[\s\S]*?<\/thinking>\s*/g, '')
.replace(/<thinking>[\s\S]*/g, '');
if (text.trim()) {
process.stdout.write(`\n${CYAN}Bot:${RESET} ${text}\n`);
hasOutput = true;
}
}
}
}
break;

case 'result':
sessionId = msg.session_id ?? sessionId;
break;
}
}
Comment thread
lishuceo marked this conversation as resolved.

stopSpinner();

// 检查是否完成
if (isOnboardingCompleted()) {
console.log(`\n${BOLD}${GREEN}配置完成!${RESET}`);
console.log(`\n${BOLD}启动服务:${RESET}`);
console.log(` npm run dev ${DIM}# 开发模式 (带 auto-reload)${RESET}`);
console.log(` pm2 start npm --name anycode -- start ${DIM}# 生产模式 (后台运行)${RESET}\n`);
break;
}

if (!hasOutput) {
console.log(`${DIM}(agent 正在处理文件操作...)${RESET}`);
}

// 读取用户输入
const input = await rl.question(`\n${YELLOW}你:${RESET} `);
if (input.trim().toLowerCase() === '退出' || input.trim().toLowerCase() === 'exit') {
console.log(`\n${DIM}配置中断。已完成的配置已保存。${RESET}`);
console.log(`运行 ${BOLD}npm run onboard${RESET} 继续配置。\n`);
break;
}
userMessage = input;

} catch (err) {
stopSpinner();
const msg = err instanceof Error ? err.message : String(err);
console.error(`\n${YELLOW}出错: ${msg}${RESET}`);

// 认证相关错误:给出清晰的修复指引
if (msg.includes('401') || msg.includes('auth') || msg.includes('authenticate') || msg.includes('API Key') || msg.includes('Unauthorized')) {
console.error(`\n${BOLD}API 认证失败。请检查:${RESET}`);
console.error(` 1. .env 中的 ANTHROPIC_API_KEY 是否正确`);
console.error(` 2. 如果使用代理,ANTHROPIC_BASE_URL 是否正确`);
console.error(` 3. API Key 是否有效且未过期\n`);
console.error(` 当前 ANTHROPIC_BASE_URL: ${process.env.ANTHROPIC_BASE_URL || '(官方地址)'}`);
console.error(` 编辑 .env 修正后重新运行: ${BOLD}npm run onboard${RESET}\n`);
break;
}

// 非致命错误,尝试继续
const retry = await rl.question(`${YELLOW}要继续吗?(Y/n): ${RESET}`);
if (retry.trim().toLowerCase() === 'n') break;
userMessage = '继续之前的配置流程。';
}
}

if (round >= MAX_ROUNDS) {
console.log(`\n${YELLOW}已达到最大轮次,配置中止。${RESET}`);
console.log(`运行 ${BOLD}npm run onboard${RESET} 继续。\n`);
}

rl.close();
Loading
Loading