小白龙 Bailongma - 初始提交
自主操作员与思考搭档系统。 包含 orchestrator-v2 多Agent编排层、后台意识引擎、记忆系统、ACUI 组件。
This commit is contained in:
BIN
orchestrator-v2/_fix2.js
Normal file
BIN
orchestrator-v2/_fix2.js
Normal file
Binary file not shown.
87
orchestrator-v2/_fix_report.js
Normal file
87
orchestrator-v2/_fix_report.js
Normal file
@@ -0,0 +1,87 @@
|
||||
const fs = require('fs');
|
||||
const reportContent = `// ============================================================
|
||||
// 辩论报告生成器
|
||||
// ============================================================
|
||||
|
||||
const fs2 = require('fs');
|
||||
|
||||
function generateReport(input, state, stepLog) {
|
||||
const now = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
||||
const report = [];
|
||||
|
||||
report.push('# 辩论总结报告');
|
||||
report.push('');
|
||||
report.push('---');
|
||||
report.push('**问题**: ' + input);
|
||||
report.push('**时间**: ' + now);
|
||||
report.push('**步骤统计**: ' + stepLog.filter(s => s.status === 'done' || s.status === 'fallback').length + '/' + stepLog.length + ' 步完成');
|
||||
report.push('');
|
||||
|
||||
report.push('## 1. 问题定义');
|
||||
report.push(state.defined || '(无)');
|
||||
report.push('');
|
||||
|
||||
report.push('## 2. 幕僚表态');
|
||||
if (state.opinions && state.opinions.length > 0) {
|
||||
for (const o of state.opinions) {
|
||||
report.push('### ' + (o.emoji || '') + ' ' + (o.persona || ''));
|
||||
report.push(o.opinion || '');
|
||||
report.push('');
|
||||
}
|
||||
} else {
|
||||
report.push('暂无幕僚表态数据');
|
||||
}
|
||||
|
||||
report.push('## 3. 冲突维度');
|
||||
if (state.dimensions && state.dimensions.length > 0) {
|
||||
state.dimensions.forEach((d, i) => {
|
||||
report.push('### 维度 ' + (i+1));
|
||||
report.push(d);
|
||||
report.push('');
|
||||
});
|
||||
} else {
|
||||
report.push('未提炼出明确的冲突维度');
|
||||
}
|
||||
|
||||
report.push('## 4. 维度辩论');
|
||||
if (state.debates && state.debates.length > 0) {
|
||||
state.debates.forEach((d, i) => {
|
||||
report.push('### 维度 ' + (i+1) + ': ' + (d.dimension || '').substring(0, 100));
|
||||
report.push(d.summary || '(无辩论摘要)');
|
||||
report.push('');
|
||||
});
|
||||
} else {
|
||||
report.push('未进行维度辩论');
|
||||
}
|
||||
|
||||
report.push('## 5. 秘书总结');
|
||||
report.push(state.summary || '(无)');
|
||||
report.push('');
|
||||
|
||||
report.push('## 6. 行动建议');
|
||||
report.push(state.harvest || '(无)');
|
||||
report.push('');
|
||||
|
||||
report.push('---');
|
||||
report.push('## 附录: 执行日志');
|
||||
if (stepLog && stepLog.length > 0) {
|
||||
for (const log of stepLog) {
|
||||
report.push('- Step ' + log.step + ': [' + log.status + '] ' + ((log.detail || '').substring(0, 80)));
|
||||
}
|
||||
}
|
||||
report.push('');
|
||||
|
||||
return report.join('\n');
|
||||
}
|
||||
|
||||
function saveReport(input, state, stepLog, filePath) {
|
||||
const report = generateReport(input, state, stepLog);
|
||||
fs2.writeFileSync(filePath, report, 'utf8');
|
||||
return filePath;
|
||||
}
|
||||
|
||||
module.exports = { generateReport, saveReport };
|
||||
`;
|
||||
|
||||
fs.writeFileSync('D:/q/Bailongma/orchestrator-v2/debate/report.js', reportContent, 'utf8');
|
||||
console.log('OK');
|
||||
58
orchestrator-v2/agent-pool.js
Normal file
58
orchestrator-v2/agent-pool.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const AgentWorker = require('./agent-worker.js');
|
||||
|
||||
class AgentPool {
|
||||
constructor(sessionStore, maxWorkers = 4) {
|
||||
this.store = sessionStore;
|
||||
this.maxWorkers = maxWorkers;
|
||||
this.workers = new Map();
|
||||
this.queue = [];
|
||||
this.results = new Map();
|
||||
}
|
||||
|
||||
async submitTask(task, context) {
|
||||
if (this.workers.size >= this.maxWorkers) {
|
||||
// Queue it
|
||||
return new Promise((resolve) => {
|
||||
this.queue.push({ task, context, resolve });
|
||||
});
|
||||
}
|
||||
return this._executeTask(task, context);
|
||||
}
|
||||
|
||||
async _executeTask(task, context) {
|
||||
const id = task.id || ('task_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8));
|
||||
this.store.createSession(id, task.name, JSON.stringify({ task, context }));
|
||||
const worker = new AgentWorker(id, task, context, this.store);
|
||||
this.workers.set(id, worker);
|
||||
const result = await worker.run();
|
||||
this.results.set(id, result);
|
||||
this.workers.delete(id);
|
||||
|
||||
// Process queue
|
||||
if (this.queue.length > 0) {
|
||||
const next = this.queue.shift();
|
||||
next.resolve(this._executeTask(next.task, next.context));
|
||||
}
|
||||
|
||||
return { id, result, status: worker.getStatus() };
|
||||
}
|
||||
|
||||
async submitAll(tasks, context) {
|
||||
const promises = tasks.map(t => this.submitTask(t, context));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
getResults() {
|
||||
return Object.fromEntries(this.results);
|
||||
}
|
||||
|
||||
getPendingCount() {
|
||||
return this.queue.length;
|
||||
}
|
||||
|
||||
getActiveCount() {
|
||||
return this.workers.size;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AgentPool;
|
||||
74
orchestrator-v2/agent-worker.js
Normal file
74
orchestrator-v2/agent-worker.js
Normal file
@@ -0,0 +1,74 @@
|
||||
const { spawn } = require("child_process");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
class AgentWorker {
|
||||
constructor(id, task, context, sessionStore) {
|
||||
this.id = id;
|
||||
this.task = task;
|
||||
this.context = context;
|
||||
this.store = sessionStore;
|
||||
this.process = null;
|
||||
this.status = "idle";
|
||||
}
|
||||
|
||||
async run() {
|
||||
this.status = "running";
|
||||
this.store.appendEvent(this.id, { type: "status", status: "running", ts: new Date().toISOString() });
|
||||
const role = this.task.roles && this.task.roles.length > 0 ? this.task.roles[0] : null;
|
||||
return new Promise((resolve) => {
|
||||
const agentCtx = { task: { id: this.task.id, name: this.task.name, target: this.task.target, priority: this.task.priority },
|
||||
role: role ? { id: role.id, name: role.name, emoji: role.emoji, description: role.description, prompt: role.prompt } : null,
|
||||
llmConfig: { baseUrl: (process.env.LLM_BASE_URL || process.env.OPENAI_BASE_URL || "https://api.openai.com/v1").replace(/\/+$/, ""),
|
||||
model: process.env.LLM_MODEL || process.env.OPENAI_MODEL || "gpt-4o",
|
||||
apiKey: process.env.LLM_API_KEY || process.env.OPENAI_API_KEY || "" },
|
||||
timestamp: new Date().toISOString() };
|
||||
const ctxJson = JSON.stringify(agentCtx);
|
||||
let script = "const ctx = " + ctxJson + ";\n";
|
||||
script += "async function runAgent() {\n";
|
||||
script += "const steps = [];\n";
|
||||
script += "const startTime = Date.now();\n";
|
||||
script += "if (ctx.role) {\n";
|
||||
script += "steps.push({ type: \"identity\", role: ctx.role.name, emoji: ctx.role.emoji, content: \"Assuming role: \" + ctx.role.emoji + \" \" + ctx.role.name });\n";
|
||||
script += "}\n";
|
||||
script += "const messages = [];\n";
|
||||
script += "if (ctx.role && ctx.role.prompt) {\n";
|
||||
script += "messages.push({ role: \"system\", content: ctx.role.prompt + \"\\n\\n\" + ctx.role.name + \"\" + ctx.role.description });\n";
|
||||
script += "} else {\n";
|
||||
script += "messages.push({ role: \"system\", content: \"You are a professional AI assistant.\" });\n";
|
||||
script += "}\n";
|
||||
script += "messages.push({ role: \"user\", content: ctx.task.target });\n";
|
||||
script += "steps.push({ type: \"llm\", action: \"calling \" + ctx.llmConfig.model });\n";
|
||||
script += "let llmResponse = \"\";\n";
|
||||
script += "let modelInfo = ctx.llmConfig.model;\n";
|
||||
script += "let errorInfo = null;\n";
|
||||
script += "try {\n";
|
||||
script += "const url = ctx.llmConfig.baseUrl + \"/chat/completions\";\n";
|
||||
script += "const r = await fetch(url, { method: \"POST\", headers: { \"Content-Type\": \"application/json\", \"Authorization\": \"Bearer \" + ctx.llmConfig.apiKey }, body: JSON.stringify({ model: ctx.llmConfig.model, messages: messages, max_tokens: 2048, temperature: 0.7 }) });\n";
|
||||
script += "if (!r.ok) { const e = await r.text().catch(()=>\"\"); throw new Error(\"LLM \" + r.status + \": \" + e.slice(0,200)); }\n";
|
||||
script += "const d = await r.json();\n";
|
||||
script += "modelInfo = d.model || ctx.llmConfig.model;\n";
|
||||
script += "llmResponse = d.choices && d.choices[0] && d.choices[0].message ? d.choices[0].message.content : JSON.stringify(d);\n";
|
||||
script += "} catch (err) { errorInfo = err.message; llmResponse = \"[LLM Error] \" + err.message; }\n";
|
||||
script += "const elapsed = Date.now() - startTime;\n";
|
||||
script += "steps.push({ type: \"llm_result\", duration: elapsed + \"ms\", model: modelInfo, error: errorInfo });\n";
|
||||
script += "const result = { summary: (ctx.role ? ctx.role.emoji + \" [\" + ctx.role.name + \"] \" : \"\") + \"Processed: \" + ctx.task.name, steps: steps, output: (ctx.role ? \"=== \" + ctx.role.name + \" Analysis ===\\n\" : \"\") + llmResponse, roleUsed: ctx.role ? ctx.role.id : null, model: modelInfo, duration: elapsed + \"ms\", error: errorInfo };\n";
|
||||
script += "process.stdout.write(JSON.stringify(result));\n";
|
||||
script += "}\n";
|
||||
script += "runAgent().then(() => process.exit(0)).catch(e => { process.stderr.write(e.message); process.exit(1); });\n";
|
||||
const child = spawn("node", ["-e", script]);
|
||||
let output = "";
|
||||
child.stdout.on("data", (d) => { output += d; });
|
||||
child.stderr.on("data", () => {});
|
||||
child.on("close", (code) => {
|
||||
this.status = code === 0 ? "completed" : "failed";
|
||||
this.store.updateStatus(this.id, this.status, output);
|
||||
this.store.appendEvent(this.id, { type: "status", status: this.status, ts: new Date().toISOString() });
|
||||
try { resolve(JSON.parse(output)); }
|
||||
catch { resolve({ summary: this.task.name + " (fallback)", raw: output.slice(0,500), status: this.status }); }
|
||||
});
|
||||
});
|
||||
}
|
||||
getStatus() { return this.status; }
|
||||
}
|
||||
module.exports = AgentWorker;
|
||||
133
orchestrator-v2/background-review.js
Normal file
133
orchestrator-v2/background-review.js
Normal file
@@ -0,0 +1,133 @@
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// BackgroundReview — post-task self-improvement evaluation
|
||||
// Forks a lightweight sub-agent to analyze completed work and
|
||||
// decide if any memory or skill updates are warranted.
|
||||
class BackgroundReview {
|
||||
constructor(config) {
|
||||
this.config = config || {};
|
||||
this.reviewDir = config.reviewDir || path.join(__dirname, 'reviews');
|
||||
this.enabled = config.enabled !== false;
|
||||
this.minResultLength = config.minResultLength || 50;
|
||||
}
|
||||
|
||||
async review(sessionId, task, result, memoryProvider) {
|
||||
if (!this.enabled) return;
|
||||
if (!result) return;
|
||||
|
||||
var output = '';
|
||||
if (typeof result === 'string') output = result;
|
||||
else if (result.output) output = typeof result.output === 'string' ? result.output : JSON.stringify(result.output);
|
||||
else if (result.summary) output = result.summary;
|
||||
else output = JSON.stringify(result);
|
||||
|
||||
if (!output || output.length < this.minResultLength) return;
|
||||
|
||||
// 1. Check for style/preference signals
|
||||
var styleSignals = this._detectStyleSignals(task, output);
|
||||
if (styleSignals.length && memoryProvider) {
|
||||
for (var i = 0; i < styleSignals.length; i++) {
|
||||
var sig = styleSignals[i];
|
||||
memoryProvider.addEntry('preference', sig.title, sig.content, ['style', 'preference']);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check for knowledge/skill signals
|
||||
var skillSignals = this._detectSkillSignals(task, output);
|
||||
if (skillSignals.length && memoryProvider) {
|
||||
for (var i = 0; i < skillSignals.length; i++) {
|
||||
var sig = skillSignals[i];
|
||||
memoryProvider.addEntry('skill_signal', sig.title, sig.content, ['skill', sig.category || 'general']);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Log review report
|
||||
this._logReview(sessionId, {
|
||||
styleSignals: styleSignals.length,
|
||||
skillSignals: skillSignals.length,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return {
|
||||
styleSignals: styleSignals.length,
|
||||
skillSignals: skillSignals.length
|
||||
};
|
||||
}
|
||||
|
||||
// Detect user preference/style signals from task output
|
||||
_detectStyleSignals(task, output) {
|
||||
var signals = [];
|
||||
var taskStr = typeof task === 'string' ? task : (task && task.task ? (typeof task.task === 'string' ? task.task : task.task.name || '') : '');
|
||||
|
||||
// Look for explicit style corrections in output
|
||||
var stylePatterns = [
|
||||
{ pattern: /别啰嗦|简洁|简短|直接点|说重点/i, title: '偏好简洁回复', content: '用户偏好简洁直接的回复风格,避免啰嗦和冗余解释', category: 'style' },
|
||||
{ pattern: /不要问|别问|直接做|别废话/i, title: '偏好行动而非询问', content: '用户希望直接执行任务,避免多余的确认性提问', category: 'style' },
|
||||
{ pattern: /用中文|说中文/i, title: '偏好中文回复', content: '用户偏好使用中文进行交流', category: 'language' },
|
||||
{ pattern: /格式|排版|对齐|美观/i, title: '关注输出格式', content: '用户关注输出格式和排版美观度', category: 'style' },
|
||||
{ pattern: /不要[格格格式]式|别用[格格格式]式|换个格式/i, title: '格式偏好调整', content: '用户对特定输出格式有偏好,需要调整回复格式', category: 'style' },
|
||||
];
|
||||
|
||||
for (var i = 0; i < stylePatterns.length; i++) {
|
||||
if (stylePatterns[i].pattern.test(taskStr) || stylePatterns[i].pattern.test(output)) {
|
||||
signals.push(stylePatterns[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return signals;
|
||||
}
|
||||
|
||||
// Detect reusable knowledge/skill signals from task output
|
||||
_detectSkillSignals(task, output) {
|
||||
var signals = [];
|
||||
var combined = (typeof task === 'string' ? task : JSON.stringify(task)) + ' ' + output;
|
||||
|
||||
// Look for knowledge that should be saved
|
||||
var knowledgePatterns = [
|
||||
{ pattern: /架构|设计模式|最佳实践|方案设计/i, title: '架构知识', content: '任务产生了架构设计或技术方案', category: 'architecture' },
|
||||
{ pattern: /工作流|流程|步骤|指南/i, title: '工作流知识', content: '任务产生了可复用的工作流或操作步骤', category: 'workflow' },
|
||||
{ pattern: /配置|部署|安装|docker/i, title: '部署知识', content: '任务涉及环境配置或部署流程', category: 'devops' },
|
||||
{ pattern: /API|接口|路由|端点/i, title: 'API知识', content: '任务涉及API设计和接口规范', category: 'api' },
|
||||
{ pattern: /代码|实现|函数|模块/i, title: '代码实现', content: '任务涉及具体代码实现', category: 'code' },
|
||||
{ pattern: /测试|调试|修复|bug/i, title: '调试知识', content: '任务涉及问题排查或bug修复', category: 'debug' },
|
||||
];
|
||||
|
||||
for (var i = 0; i < knowledgePatterns.length; i++) {
|
||||
if (knowledgePatterns[i].pattern.test(combined)) {
|
||||
signals.push(knowledgePatterns[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return signals;
|
||||
}
|
||||
|
||||
_logReview(sessionId, report) {
|
||||
if (!fs.existsSync(this.reviewDir)) fs.mkdirSync(this.reviewDir, { recursive: true });
|
||||
var logFile = path.join(this.reviewDir, 'reviews.jsonl');
|
||||
var line = JSON.stringify({ sessionId: sessionId, report: report, ts: new Date().toISOString() }) + '\n';
|
||||
try { fs.appendFileSync(logFile, line, 'utf8'); } catch (e) {}
|
||||
}
|
||||
|
||||
getReviewStats() {
|
||||
var logFile = path.join(this.reviewDir, 'reviews.jsonl');
|
||||
if (!fs.existsSync(logFile)) return { total: 0, styleSignals: 0, skillSignals: 0 };
|
||||
try {
|
||||
var lines = fs.readFileSync(logFile, 'utf8').split('\n').filter(Boolean);
|
||||
var stats = { total: lines.length, styleSignals: 0, skillSignals: 0 };
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
try {
|
||||
var entry = JSON.parse(lines[i]);
|
||||
if (entry.report) {
|
||||
stats.styleSignals += entry.report.styleSignals || 0;
|
||||
stats.skillSignals += entry.report.skillSignals || 0;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
return stats;
|
||||
} catch (e) { return { total: 0, styleSignals: 0, skillSignals: 0 }; }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BackgroundReview;
|
||||
149
orchestrator-v2/context-compressor.js
Normal file
149
orchestrator-v2/context-compressor.js
Normal file
@@ -0,0 +1,149 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// ContextCompressor — automatic context window compression
|
||||
// Monitors session token usage and produces structured summaries
|
||||
// when token budgets are exceeded. Supports iterative compression
|
||||
// (compressed sessions can be further compressed).
|
||||
class ContextCompressor {
|
||||
constructor(config) {
|
||||
this.config = config || {};
|
||||
this.maxTokensBeforeCompress = config.maxTokensBeforeCompress || 20000;
|
||||
this.minCompressionGain = config.minCompressionGain || 0.1; // 10% minimum gain
|
||||
this.summaryTemplate = config.summaryTemplate || this._defaultTemplate();
|
||||
}
|
||||
|
||||
_defaultTemplate() {
|
||||
return {
|
||||
activeTask: '',
|
||||
goal: '',
|
||||
progress: [],
|
||||
decisions: [],
|
||||
blockers: [],
|
||||
openQuestions: [],
|
||||
keyFiles: [],
|
||||
remainingWork: [],
|
||||
notes: ''
|
||||
};
|
||||
}
|
||||
|
||||
// Check if a session needs compression
|
||||
shouldCompress(sessionStore, sessionId) {
|
||||
var tokenCount = sessionStore.getTotalTokenCount(sessionId);
|
||||
return tokenCount > this.maxTokensBeforeCompress;
|
||||
}
|
||||
|
||||
// Compress a session: produce summary and mark as compressed
|
||||
async compress(sessionStore, sessionId, childSessionId) {
|
||||
var session = sessionStore.getSession(sessionId);
|
||||
if (!session) return null;
|
||||
|
||||
var messages = sessionStore.getMessages(sessionId);
|
||||
var summary = this._buildSummary(session, messages);
|
||||
|
||||
// Mark current session as compressed
|
||||
sessionStore.compressSession(sessionId, summary.summary, childSessionId);
|
||||
|
||||
// If there was a previous compression, propagate context
|
||||
if (session.parent_id) {
|
||||
var parentChain = sessionStore.getSessionChain(sessionId);
|
||||
summary.compressionChain = parentChain.length;
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
// Build a structured summary from session data and messages
|
||||
_buildSummary(session, messages) {
|
||||
var summary = JSON.parse(JSON.stringify(this.summaryTemplate));
|
||||
|
||||
// Extract from session metadata
|
||||
summary.activeTask = session.task || '';
|
||||
if (session.summary) {
|
||||
summary.notes = session.summary;
|
||||
}
|
||||
|
||||
// Extract from messages (last N messages for most recent context)
|
||||
var recentMsgs = messages.slice(-20);
|
||||
|
||||
for (var i = 0; i < recentMsgs.length; i++) {
|
||||
var msg = recentMsgs[i];
|
||||
var content = msg.content || '';
|
||||
|
||||
// Look for decision patterns
|
||||
var decisionMatch = content.match(/决定|选择|采用|使用|改用|确定|确认.*方案/i);
|
||||
if (decisionMatch) {
|
||||
var snippet = content.slice(Math.max(0, decisionMatch.index - 20), decisionMatch.index + 40);
|
||||
summary.decisions.push(snippet);
|
||||
}
|
||||
|
||||
// Look for blocker patterns
|
||||
var blockerMatch = content.match(/阻塞|卡住|问题|错误|失败|报错|无法|不能|不行/i);
|
||||
if (blockerMatch) {
|
||||
var snippet = content.slice(Math.max(0, blockerMatch.index - 20), blockerMatch.index + 40);
|
||||
summary.blockers.push(snippet);
|
||||
}
|
||||
|
||||
// Look for file references
|
||||
var fileMatch = content.match(/[A-Za-z]:\\[^\s,,。;;::!!??()()\[\]【】{}"']+/g);
|
||||
if (fileMatch) {
|
||||
for (var j = 0; j < fileMatch.length; j++) {
|
||||
if (summary.keyFiles.indexOf(fileMatch[j]) < 0) {
|
||||
summary.keyFiles.push(fileMatch[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for progress markers
|
||||
if (content.includes('完成') || content.includes('成功') || content.includes('通过')) {
|
||||
summary.progress.push(content.slice(0, 80));
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate and trim
|
||||
summary.decisions = this._unique(summary.decisions).slice(0, 10);
|
||||
summary.blockers = this._unique(summary.blockers).slice(0, 10);
|
||||
summary.keyFiles = this._unique(summary.keyFiles).slice(0, 15);
|
||||
summary.progress = this._unique(summary.progress).slice(0, 10);
|
||||
|
||||
// Build condensed summary string
|
||||
var summaryStr = 'Task: ' + (summary.activeTask || 'N/A');
|
||||
if (summary.progress.length) summaryStr += ' | Progress: ' + summary.progress.length + ' items';
|
||||
if (summary.decisions.length) summaryStr += ' | Decisions: ' + summary.decisions.length;
|
||||
if (summary.blockers.length) summaryStr += ' | Blockers: ' + summary.blockers.length;
|
||||
if (summary.keyFiles.length) summaryStr += ' | Files: ' + summary.keyFiles.length;
|
||||
summary.summary = summaryStr;
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
// Estimate token count for a string (approx: 1 token ~= 2 CJK chars or 4 ASCII chars)
|
||||
estimateTokens(text) {
|
||||
if (!text) return 0;
|
||||
var cjkCount = (text.match(/[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]/g) || []).length;
|
||||
var asciiCount = text.length - cjkCount;
|
||||
return Math.ceil(cjkCount / 1.5) + Math.ceil(asciiCount / 4);
|
||||
}
|
||||
|
||||
// Check compression effectiveness (anti-thrash)
|
||||
compressionGain(sessionStore, sessionId) {
|
||||
var before = sessionStore.getTotalTokenCount(sessionId);
|
||||
var session = sessionStore.getSession(sessionId);
|
||||
if (!before || !session) return 0;
|
||||
|
||||
var summaryLen = this.estimateTokens(session.summary || '');
|
||||
if (before === 0) return 0;
|
||||
|
||||
return (before - summaryLen) / before;
|
||||
}
|
||||
|
||||
_unique(arr) {
|
||||
var result = [];
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
if (result.indexOf(arr[i]) < 0) result.push(arr[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ContextCompressor;
|
||||
210
orchestrator-v2/coordinator.js
Normal file
210
orchestrator-v2/coordinator.js
Normal file
@@ -0,0 +1,210 @@
|
||||
const AgentPool = require('./agent-pool.js');
|
||||
const SessionStore = require('./session-store.js');
|
||||
const RoleRouter = require('./role-router.js');
|
||||
const { BuiltInMemoryProvider } = require('./memory-provider.js');
|
||||
const BackgroundReview = require('./background-review.js');
|
||||
const Curator = require('./curator.js');
|
||||
const ContextCompressor = require('./context-compressor.js');
|
||||
const path = require('path');
|
||||
|
||||
class Coordinator {
|
||||
constructor(config) {
|
||||
this.config = config || {};
|
||||
this.dbDir = this.config.dbDir || path.join(__dirname, 'db');
|
||||
this.store = null;
|
||||
this.pool = null;
|
||||
this.router = new RoleRouter();
|
||||
this.memoryProvider = null;
|
||||
this.reviewer = null;
|
||||
this.curator = null;
|
||||
this.compressor = null;
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.initialized) return;
|
||||
|
||||
// Layer 1: SQLite Session Store
|
||||
this.store = new SessionStore(this.dbDir);
|
||||
await this.store.init();
|
||||
|
||||
// Layer 2: Memory Provider
|
||||
this.memoryProvider = new BuiltInMemoryProvider(this.config.memory || {});
|
||||
await this.memoryProvider.initialize();
|
||||
|
||||
// Layer 3: Background Review
|
||||
this.reviewer = new BackgroundReview(this.config.review || {});
|
||||
|
||||
// Layer 4: Curator
|
||||
this.curator = new Curator(this.config.curator || {});
|
||||
|
||||
// Layer 5: Context Compressor
|
||||
this.compressor = new ContextCompressor(this.config.compressor || {});
|
||||
|
||||
// Agent Pool (uses SessionStore)
|
||||
this.pool = new AgentPool(this.store, this.config.maxWorkers || 4);
|
||||
|
||||
// Role Router
|
||||
await this.router.init();
|
||||
|
||||
this.initialized = true;
|
||||
console.log('[Coordinator v3.0] Initialized with all 5 persistence layers');
|
||||
console.log('[Coordinator] Role templates:', this.router.getTemplates().getRoleCount());
|
||||
}
|
||||
|
||||
decomposeTask(mainTask) {
|
||||
return this.router.decomposeWithRoles(mainTask);
|
||||
}
|
||||
|
||||
async run(mainTask, context) {
|
||||
if (!this.initialized) await this.init();
|
||||
if (!context) context = {};
|
||||
|
||||
var sessionId = 'session_' + Date.now();
|
||||
this.store.createSession(sessionId, mainTask.slice(0, 200), context);
|
||||
this.store.updateStatus(sessionId, 'running');
|
||||
console.log('[Coordinator] Session:', sessionId);
|
||||
|
||||
// Layer 2: Pre-fetch relevant memories
|
||||
var memoryContext = '';
|
||||
try {
|
||||
memoryContext = await this.memoryProvider.getContextBlock(mainTask);
|
||||
if (memoryContext) {
|
||||
console.log('[Coordinator] Injected', this.memoryProvider.memories.length, 'memory entries');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Coordinator] Memory prefetch error:', e.message);
|
||||
}
|
||||
|
||||
// Decompose task with role matching
|
||||
console.log('[Coordinator] Decomposing task with role matching...');
|
||||
var subTasks = await this.decomposeTask(mainTask);
|
||||
|
||||
// Inject memory context into each sub-task
|
||||
var enrichedContext = { ...context, memoryContext: memoryContext, roleEngine: true };
|
||||
|
||||
// Phase 1: Parallel execution
|
||||
console.log('[Coordinator] Executing', subTasks.length, 'sub-tasks in parallel...');
|
||||
var results = await this.pool.submitAll(subTasks, enrichedContext);
|
||||
|
||||
// Phase 2: Aggregate results
|
||||
console.log('[Coordinator] Aggregating results...');
|
||||
var aggregated = this._aggregate(results);
|
||||
|
||||
this.store.updateStatus(sessionId, 'completed', aggregated);
|
||||
|
||||
// Layer 2: Sync memories from results
|
||||
try {
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
var r = results[i];
|
||||
if (r && r.result) {
|
||||
await this.memoryProvider.syncTurn(r.task || subTasks[i], r.result);
|
||||
}
|
||||
}
|
||||
if (aggregated) {
|
||||
await this.memoryProvider.syncTurn(mainTask, aggregated);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Coordinator] Memory sync error:', e.message);
|
||||
}
|
||||
|
||||
// Layer 3: Background review
|
||||
try {
|
||||
var reviewResult = await this.reviewer.review(sessionId, mainTask, aggregated, this.memoryProvider);
|
||||
if (reviewResult && (reviewResult.styleSignals > 0 || reviewResult.skillSignals > 0)) {
|
||||
console.log('[Coordinator] Background review:', reviewResult.styleSignals + ' style signals, ' + reviewResult.skillSignals + ' skill signals');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Coordinator] Review error:', e.message);
|
||||
}
|
||||
|
||||
// Layer 5: Check if compression needed
|
||||
try {
|
||||
if (this.compressor.shouldCompress(this.store, sessionId)) {
|
||||
console.log('[Coordinator] Session exceeds compression threshold, compressing...');
|
||||
var compressed = await this.compressor.compress(this.store, sessionId);
|
||||
if (compressed) {
|
||||
console.log('[Coordinator] Compression complete:', compressed.summary);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Coordinator] Compression error:', e.message);
|
||||
}
|
||||
|
||||
return { sessionId, subTasks: results, aggregated };
|
||||
}
|
||||
|
||||
_aggregate(results) {
|
||||
var summaries = results.map(function(r) {
|
||||
return {
|
||||
id: r.id || r.task || '',
|
||||
status: r.status || (r.result ? 'completed' : 'failed'),
|
||||
summary: r.result ? (r.result.summary || '') : 'no result',
|
||||
output: r.result ? (r.result.output || '') : ''
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
totalTasks: results.length,
|
||||
completed: results.filter(function(r) { return r.status === 'completed' || (r.result && r.result.status !== 'failed'); }).length,
|
||||
failed: results.filter(function(r) { return r.status === 'failed' || (r.result && r.result.status === 'failed'); }).length,
|
||||
summaries: summaries
|
||||
};
|
||||
}
|
||||
|
||||
// Layer 4: Run curator
|
||||
async runCurator() {
|
||||
if (!this.initialized) await this.init();
|
||||
console.log('[Coordinator] Running curator...');
|
||||
var report = await this.curator.run(this.memoryProvider);
|
||||
console.log('[Coordinator] Curator report: ' + report.staleSkills.length + ' stale, ' + report.archivedSkills.length + ' archive candidates');
|
||||
if (report.memoryAnalysis && report.memoryAnalysis.suggestions.length) {
|
||||
console.log('[Coordinator] Memory suggestions:', report.memoryAnalysis.suggestions.join('; '));
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
// Search across all persistence layers
|
||||
async search(query, limit) {
|
||||
if (!this.initialized) await this.init();
|
||||
if (limit === undefined) limit = 10;
|
||||
var results = {
|
||||
sessions: this.store.searchSessions(query, limit),
|
||||
messages: this.store.searchMessages(query, limit * 2),
|
||||
memories: []
|
||||
};
|
||||
try {
|
||||
results.memories = await this.memoryProvider.search(query, limit);
|
||||
} catch (e) {}
|
||||
return results;
|
||||
}
|
||||
|
||||
// Get system stats
|
||||
async getStats() {
|
||||
if (!this.initialized) await this.init();
|
||||
return {
|
||||
sessions: this.store.getStats(),
|
||||
memories: this.memoryProvider.getStats(),
|
||||
reviews: this.reviewer.getReviewStats()
|
||||
};
|
||||
}
|
||||
|
||||
// List recent sessions
|
||||
listSessions(limit) {
|
||||
if (!this.store) return [];
|
||||
return this.store.listSessions(limit || 10);
|
||||
}
|
||||
|
||||
// Get compressed session chain
|
||||
getSessionChain(sessionId) {
|
||||
if (!this.store) return [];
|
||||
return this.store.getSessionChain(sessionId);
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (this.memoryProvider) await this.memoryProvider.shutdown();
|
||||
if (this.store) this.store.close();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Coordinator;
|
||||
141
orchestrator-v2/curator.js
Normal file
141
orchestrator-v2/curator.js
Normal file
@@ -0,0 +1,141 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Curator — background skill maintenance orchestrator
|
||||
// Periodically reviews agent-created skills and memories for
|
||||
// consolidation, staleness, and merge opportunities.
|
||||
class Curator {
|
||||
constructor(config) {
|
||||
this.config = config || {};
|
||||
this.skillsDir = config.skillsDir || path.join(process.env.HOME || process.env.USERPROFILE || '.', '.hermes', 'skills');
|
||||
this.reportDir = config.reportDir || path.join(__dirname, 'reports');
|
||||
this.staleDays = config.staleDays || 30;
|
||||
this.archiveDays = config.archiveDays || 90;
|
||||
this.enabled = config.enabled !== false;
|
||||
}
|
||||
|
||||
async run(memoryProvider) {
|
||||
if (!this.enabled) return { status: 'disabled' };
|
||||
|
||||
var report = {
|
||||
timestamp: new Date().toISOString(),
|
||||
staleSkills: [],
|
||||
archivedSkills: [],
|
||||
mergedSuggestions: [],
|
||||
memoryAnalysis: null
|
||||
};
|
||||
|
||||
// 1. Check skill staleness
|
||||
try {
|
||||
var skills = this._scanSkills();
|
||||
report.staleSkills = this._checkStaleness(skills);
|
||||
report.archivedSkills = this._checkArchive(skills);
|
||||
} catch (e) {
|
||||
report.skillError = e.message;
|
||||
}
|
||||
|
||||
// 2. Analyze memories for consolidation opportunities
|
||||
if (memoryProvider) {
|
||||
try {
|
||||
report.memoryAnalysis = this._analyzeMemories(memoryProvider);
|
||||
} catch (e) {
|
||||
report.memoryError = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Generate report
|
||||
this._saveReport(report);
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
_scanSkills() {
|
||||
if (!fs.existsSync(this.skillsDir)) return [];
|
||||
var entries = [];
|
||||
try {
|
||||
var items = fs.readdirSync(this.skillsDir, { withFileTypes: true });
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (items[i].isDirectory() || items[i].name.endsWith('.md')) {
|
||||
var fullPath = path.join(this.skillsDir, items[i].name);
|
||||
var stat = fs.statSync(fullPath);
|
||||
entries.push({
|
||||
name: items[i].name,
|
||||
path: fullPath,
|
||||
isDir: items[i].isDirectory(),
|
||||
created: stat.birthtime,
|
||||
modified: stat.mtime,
|
||||
ageDays: (Date.now() - stat.mtime.getTime()) / 86400000
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return entries;
|
||||
}
|
||||
|
||||
_checkStaleness(skills) {
|
||||
return skills.filter(function(s) {
|
||||
return s.ageDays > this.staleDays && s.ageDays < this.archiveDays;
|
||||
}.bind(this)).map(function(s) {
|
||||
return { name: s.name, ageDays: Math.round(s.ageDays), action: 'mark-stale' };
|
||||
});
|
||||
}
|
||||
|
||||
_checkArchive(skills) {
|
||||
return skills.filter(function(s) {
|
||||
return s.ageDays > this.archiveDays;
|
||||
}.bind(this)).map(function(s) {
|
||||
return { name: s.name, ageDays: Math.round(s.ageDays), action: 'archive' };
|
||||
});
|
||||
}
|
||||
|
||||
_analyzeMemories(memoryProvider) {
|
||||
var stats = memoryProvider.getStats();
|
||||
var suggestions = [];
|
||||
|
||||
// Check for memory type balance
|
||||
if (stats.byType) {
|
||||
var total = stats.total || 0;
|
||||
if (total > 50) {
|
||||
suggestions.push('记忆总数超过50条,建议整理合并相似条目');
|
||||
}
|
||||
var taskResults = stats.byType.task_result || 0;
|
||||
if (taskResults > 20) {
|
||||
suggestions.push('task_result 类型记忆过多(' + taskResults + '条),建议按项目归类');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalMemories: stats.total || 0,
|
||||
byType: stats.byType || {},
|
||||
suggestions: suggestions
|
||||
};
|
||||
}
|
||||
|
||||
_saveReport(report) {
|
||||
if (!fs.existsSync(this.reportDir)) fs.mkdirSync(this.reportDir, { recursive: true });
|
||||
var ts = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
var reportPath = path.join(this.reportDir, 'curator-' + ts + '.json');
|
||||
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
getReportHistory(limit) {
|
||||
if (limit === undefined) limit = 5;
|
||||
if (!fs.existsSync(this.reportDir)) return [];
|
||||
try {
|
||||
var files = fs.readdirSync(this.reportDir)
|
||||
.filter(function(f) { return f.startsWith('curator-') && f.endsWith('.json'); })
|
||||
.sort()
|
||||
.reverse()
|
||||
.slice(0, limit);
|
||||
var reports = [];
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
try {
|
||||
reports.push(JSON.parse(fs.readFileSync(path.join(this.reportDir, files[i]), 'utf8')));
|
||||
} catch (e) {}
|
||||
}
|
||||
return reports;
|
||||
} catch (e) { return []; }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Curator;
|
||||
34
orchestrator-v2/db/schema.sql
Normal file
34
orchestrator-v2/db/schema.sql
Normal file
@@ -0,0 +1,34 @@
|
||||
-- orchestrator-v2 会话持久化 SQLite schema
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
parent_id TEXT,
|
||||
task TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'created', -- created | running | paused | completed | failed
|
||||
context TEXT, -- JSON: 完整上下文
|
||||
result TEXT, -- JSON: 最终结果
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (parent_id) REFERENCES sessions(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL,
|
||||
type TEXT NOT NULL, -- think | tool_call | tool_result | message | progress
|
||||
data TEXT NOT NULL, -- JSON
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agent_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL,
|
||||
agent_name TEXT,
|
||||
level TEXT DEFAULT 'info',
|
||||
message TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_sessions_status ON sessions(status);
|
||||
CREATE INDEX idx_events_session ON events(session_id);
|
||||
BIN
orchestrator-v2/db/sessions.sqlite
Normal file
BIN
orchestrator-v2/db/sessions.sqlite
Normal file
Binary file not shown.
358
orchestrator-v2/debate-report.md
Normal file
358
orchestrator-v2/debate-report.md
Normal file
@@ -0,0 +1,358 @@
|
||||
# Debate Summary Report
|
||||
|
||||
---
|
||||
**Question**: 我该不该全职做独立开发
|
||||
**Time**: 2026-05-22 09:52:32
|
||||
**Steps**: 7/21
|
||||
|
||||
# Problem Definition
|
||||
问题目前比较模糊。为了帮你把“该不该全职做独立开发”变成可辩论的议题,需要先明确两个关键信息:
|
||||
|
||||
1. **你的财务缓冲期有多长?**(比如:在不依赖独立开发收入的情况下,现有存款能支撑你生活几个月?)
|
||||
2. **你的“独立开发”目前处于什么阶段?**(是已经有上线产品且有少量收入,还是刚有想法、代码还没开始写?)
|
||||
|
||||
请先补充这两点,我们再来聚焦判断标准。
|
||||
|
||||
# Advisor Opinions
|
||||
### 🍏 乔布斯
|
||||
**结论:不该全职。**
|
||||
你的回答里没有存款、没有稳定收入、没有产品验证——这是自杀式创业。
|
||||
|
||||
**核心论点:**
|
||||
1. **没有财务缓冲,你无法专注创造伟大产品。** 我当年在车库里做Apple I,靠的是卖计算器攒的钱和稳定的兼职收入。饥饿只会让你妥协,而不是创新。
|
||||
2. **独立开发需要“已证明的需求”,而不是“想法”。** 你甚至没说出一个上线产品或有100美元收入。先兼职做出最小可行产品,用真实用户的反馈验证,再谈全职。
|
||||
|
||||
**行动建议:** 保留工作,每天挤出4小时开发。等产品月收入达到你当前月薪的50%时,再辞职。否则,你只是在赌运气,而不是在创造。
|
||||
|
||||
### 📝 Paul Graham
|
||||
不要全职做独立开发——除非你已有产品上线且用户增长曲线呈自然上升趋势。
|
||||
|
||||
你的问题缺少两个关键数据:财务缓冲期和产品阶段。如果你存款撑不过12个月,或产品还没上线/月收入低于100美元,全职就是自杀式赌博。真正的独立开发是“先有惊喜用户,再全职投入”,而不是反过来。
|
||||
|
||||
核心论点:**好的创业想法会逼你辞职,而不是让你纠结该不该辞职**。如果你还在犹豫,说明产品还没验证到足够强的需求信号。先兼职跑通最小闭环,让早期用户告诉你“这必须存在”,再全情投入。
|
||||
|
||||
### 🚀 马斯克
|
||||
根据第一性原理:**你的存款能支撑你多久不依赖收入生活?** 如果少于12个月,别全职。独立开发不是“逃离工作”,而是用最小可行产品验证市场需求。你目前有产品吗?月收入能覆盖你1/3的生活成本吗?没有的话,先兼职做到有付费用户,再谈全职。大多数人的“全职创业”只是逃避低效工作的幻觉。
|
||||
|
||||
### 🧘 Naval
|
||||
从你的视角(Naval),**不该全职做独立开发**,除非你已拥有“代码杠杆”且能规模化。
|
||||
|
||||
核心论点:
|
||||
1. **财富是拥有,不是出卖时间**。全职独立开发本质仍是“用时间换钱”,不是真正的杠杆(除非你已有产品且用户增长可自动化)。否则你只是换了个地方打工。
|
||||
2. **生存优先于自由**。你问“该不该”时,说明财务缓冲不足。真正自由的人不会问——他们已用资产或代码产生被动收入。先积累6-12个月生活费,再考虑全职。
|
||||
|
||||
结论:**保持正职,用业余时间验证产品是否可规模化**。当独立开发收入超过正职2倍且稳定时,才值得全职。
|
||||
|
||||
### 🧠 芒格
|
||||
根据你的问题,我给出逆向判断:**不该全职做独立开发,除非你已通过“生存测试”**。
|
||||
|
||||
核心论点:
|
||||
1. **财务缓冲不足**:若存款无法支撑12个月以上无收入生活,全职就是赌命。独立开发收入极不稳定,生存压力会扭曲决策——你会急于变现而非打磨价值。
|
||||
2. **能力圈未验证**:若过去3个月项目未产生超过100美元收入,说明产品尚未找到市场匹配。全职只会加速错误,而非解决根本问题。
|
||||
|
||||
**结论**:保持现有收入来源,用业余时间验证到月收入能覆盖基本生活支出后,再考虑全职。否则,你是在用焦虑换自由。
|
||||
|
||||
### 🔬 费曼
|
||||
费曼式回答:别问“该不该”,问“能不能”。你的独立开发项目如果过去三个月没有产生任何一笔超过100美元的收入,那它目前只是个爱好,不是生意。全职做爱好等于赌博。核心论点:**用物理原理检验——没有收入验证的假设,就是空想。** 先兼职做到月收入覆盖你基本生活开支的50%,再考虑全职。否则你只是在用存款买“自由”的幻觉。
|
||||
|
||||
### 🦢 塔勒布
|
||||
塔勒布式判断:**不要全职。**
|
||||
|
||||
1. **你暴露在脆弱性中**:全职意味着你的生存完全依赖一个尚未验证的“尾部收益”——独立开发的成功概率极低,而一旦失败,你连生活都维持不了。这违背了“不要赌上吃饭钱”的原则。
|
||||
|
||||
2. **反脆弱的做法**:保持有稳定收入的正职,利用业余时间做独立开发。这样,如果项目失败,你只是损失了时间(可控的波动);如果成功,你获得巨大收益(正向黑天鹅)。你从不确定性中获利,而非被它击垮。
|
||||
|
||||
**结论**:只有当你现有存款能支撑你2年以上无收入生活,且项目已产生稳定现金流(覆盖月生活费的50%以上),才考虑全职。否则,你是在押注而非创业。
|
||||
|
||||
### 💰 特朗普
|
||||
特朗普式回答:
|
||||
|
||||
**结论:别全职干,除非你有6个月以上的“生存现金”+一笔超过100美元的订单。**
|
||||
**核心论点:**
|
||||
1. **杠杆不够**——没有稳定收入或明确订单前,全职是赌博。你需要先做出“赢”的迹象(比如月入3000美元),否则谈判地位为零。
|
||||
2. **制造压力不如制造筹码**——兼职干到产品有用户、有收入,再拿数据去跟市场或投资人谈。没有交易筹码,你只是另一个破产的梦想家。
|
||||
|
||||
**行动:** 先兼职做到月入覆盖房租,再考虑全职。否则,你就是在给自己挖坑。
|
||||
|
||||
### 🤖 Karpathy
|
||||
不建议全职。核心论点:
|
||||
|
||||
1. **没有收入验证的独立开发是赌博。** 你过去三个月连一笔超过100美元的收入都没有,说明产品连最小市场验证都没通过。全职只会加速消耗存款,而不是解决产品问题。
|
||||
2. **用业余时间把MVP跑通再谈全职。** 真正的独立开发者是先用周末和晚上把产品做到有人付费、有正向反馈,才敢辞职。你现在连代码都没跑通用户闭环,全职只会让你在焦虑中写出更烂的代码。
|
||||
|
||||
结论:继续上班,用业余时间把产品推到有稳定收入(比如月入覆盖基本生活)再考虑全职。
|
||||
|
||||
### 🧬 Ilya Sutskever
|
||||
你不该全职做独立开发——除非你已确认产品在某个明确方向上存在“涌现可能”且你具备至少18个月的财务缓冲。
|
||||
|
||||
**核心论点:** 独立开发的成功依赖持续的认知投入与迭代,而财务压力会打断你对长期趋势的判断力。若你的产品尚未产生任何超过100美元的收入,说明它仍处于“预缩放”阶段,此时全职只会加速耗尽你的生存资源,而非提升突破概率。先保持兼职,直到你观察到明确的用户增长拐点或收入信号。
|
||||
|
||||
### 🎬 MrBeast
|
||||
别全职。你连一笔100美元都没赚到,说明产品还没验证市场。独立开发本质是内容创业,没有用户疯传的“爆点”就毫无意义。先兼职做到月收入覆盖基本生活开支,再考虑all in。核心论点:**用最小成本测试病毒系数**,未验证前全职就是赌博。
|
||||
|
||||
### 📱 张一鸣
|
||||
张一鸣的判断:
|
||||
|
||||
**结论:不建议全职。**
|
||||
核心论点1:**数据不足,风险不可控。** 你连“过去三个月是否有超过100美元收入”都不确定,说明产品尚未验证市场匹配度。全职后没有反馈闭环,容易陷入低效闭门造车。
|
||||
核心论点2:**系统优于个人意志。** 真正可规模化的是用低成本(兼职+工具)跑通最小闭环,靠数据迭代而非赌上全部时间。先兼职做到月收入覆盖基本生活成本,再考虑全职。
|
||||
|
||||
# Conflict Dimensions
|
||||
### Dimension 1
|
||||
好的,以下是从各位幕僚的表态中识别出的核心冲突维度:
|
||||
|
||||
### Dimension 2
|
||||
冲突核心:**全职独立开发的前提是“生存验证”还是“市场验证”?**
|
||||
建议焦点:分歧在于,决定是否全职的关键门槛是该先确保个人财务安全(如存款能撑12个月),还是该先证明产品能产生收入(如月收入达到生活成本的50%)。乔布斯、马斯克、Naval强调财务缓冲是基础,而Paul Graham、费曼、MrBeast更看重产品是否已产生真实的付费用户和市场信号。
|
||||
|
||||
### Dimension 3
|
||||
冲突核心:**全职投入是加速成功,还是加速失败?**
|
||||
建议焦点:一方认为全职能让人专注、提升产出效率(如乔布斯隐含的“专注创造伟大产品”);另一方认为,在没有验证需求前,全职只会加速消耗存款,并在焦虑中做出更差的决策(如Karpathy、Ilya认为财务压力会打断判断力,芒格认为生存压力会扭曲决策)。
|
||||
|
||||
### Dimension 4
|
||||
冲突核心:**独立开发的本质是“用时间换钱”还是“创造可规模化的杠杆”?**
|
||||
建议焦点:Naval明确提出,若产品无法自动化增长,全职独立开发本质上仍是“出卖时间”,只是换了个地方打工;而马斯克、张一鸣等更强调通过最小可行产品快速验证市场,认为“可规模化的系统”才是目标,而非单纯投入时间。
|
||||
|
||||
### Dimension 5
|
||||
冲突核心:**决策依据应基于“理性计算”还是“信号直觉”?**
|
||||
建议焦点:塔勒布、芒格、张一鸣强调用“生存测试”、“反脆弱性”、“数据闭环”等理性框架做决策;而Paul Graham、Ilya则更关注“用户增长的自然曲线”或“涌现可能”这类动态信号,认为好的想法会“逼你辞职”,而非靠计算得出。
|
||||
|
||||
### Dimension 6
|
||||
冲突核心:**“兼职验证”的充分条件是什么?是“有收入”还是“有爆发潜力”?**
|
||||
建议焦点:多数人认为兼职阶段需要做到“月收入覆盖基本生活”(如乔布斯、费曼、MrBeast);但MrBeast特别强调“病毒系数”和“用户疯传的爆点”,Naval则关注“代码杠杆能否规模化”,说明对“验证成功”的定义存在差异——是追求稳定现金流,还是追求指数级增长潜力。
|
||||
|
||||
# Dimension Debates
|
||||
### Dimension 1: 好的,以下是从各位幕僚的表态中识别出的核心冲突维度:
|
||||
**核心矛盾**:是否应在产品验证充分之前,以“破釜沉舟”的专注换取爆发可能,还是必须先有市场信号与财务安全再All-in。
|
||||
|
||||
**支持方核心观点**:伟大产品源于极端专注与风险承担,若已有基础存款和初步收入(哪怕小额),全职才能集中火力打磨出颠覆性体验,避免半吊子投入错过窗口期。
|
||||
|
||||
**反对方核心观点**:独立开发本质是可持续系统而非赌博,必须在存款充足(至少12-18个月)且产品已验证(月收入覆盖基本生活)的前提下全职,否则生存压力会扭曲判断,反脆弱性才是长期主义的关键。
|
||||
|
||||
### Dimension 2: 冲突核心:**全职独立开发的前提是“生存验证”还是“市场验证”?**
|
||||
建议焦点:分歧在于,决定是否全职的关键门槛是该先确保个人财务安全(如存款能撑12个月),还是该先证明产品能产生收入(如月收入达到生
|
||||
**核心矛盾**:独立开发全职化的首要门槛,究竟是“个人财务安全”还是“产品市场信号”。
|
||||
|
||||
**支持方核心观点**:财务缓冲是生存底线,确保专注与长期判断力,避免因生存焦虑导致短视决策,是创新的物理前提。
|
||||
|
||||
**反对方核心观点**:市场验证是创业的本质信号,哪怕微小收入也证明需求真实,否则全职只是用时间换安全感,而非创造价值。
|
||||
|
||||
### Dimension 3: 冲突核心:**全职投入是加速成功,还是加速失败?**
|
||||
建议焦点:一方认为全职能让人专注、提升产出效率(如乔布斯隐含的“专注创造伟大产品”);另一方认为,在没有验证需求前,全职只会加速消耗存款,并在焦虑
|
||||
**核心矛盾**:全职投入究竟是放大专注力以加速成功,还是放大生存压力以加速失败。
|
||||
|
||||
**支持方核心观点**:全职投入能通过极致专注、快速迭代和财务压力倒逼核心价值聚焦,是已验证需求后加速成功的最优路径。
|
||||
|
||||
**反对方核心观点**:在未验证产品-市场匹配前,全职只会加速消耗存款、扭曲判断力,导致在焦虑中做出更差决策,应先兼职低成本验证闭环再考虑全职。
|
||||
|
||||
### Dimension 4: 冲突核心:**独立开发的本质是“用时间换钱”还是“创造可规模化的杠杆”?**
|
||||
建议焦点:Naval明确提出,若产品无法自动化增长,全职独立开发本质上仍是“出卖时间”,只是换了个地方打工;而马斯克、张一
|
||||
### 核心冲突维度总结
|
||||
|
||||
**核心矛盾**:
|
||||
独立开发的本质究竟是“通过个人时间投入换取线性收入(变相打工)”,还是“构建可脱离个人而自动增长的系统(创造杠杆)”。
|
||||
|
||||
**支持方核心观点**(乔布斯、马斯克、张一鸣、费曼等):
|
||||
独立开发的核心是**构建可规模化的杠杆**——通过最小可行产品快速验证市场,让产品、代码或自动化系统脱离个人时间也能增长,否则全职投入只是自我欺骗的“高级打工”。
|
||||
|
||||
**反对方核心观点**(Naval、塔勒布):
|
||||
若产品无法实现自动化增长,全职独立开发**本质仍是“用时间换钱”**,只是换了个工位承担更高风险;真正的独立开发必须优先设计反脆弱系统,避免将生存押注在未验证的线性投入上。
|
||||
|
||||
### Dimension 5: 冲突核心:**决策依据应基于“理性计算”还是“信号直觉”?**
|
||||
建议焦点:塔勒布、芒格、张一鸣强调用“生存测试”、“反脆弱性”、“数据闭环”等理性框架做决策;而Paul Graham、Ilya则更关注
|
||||
**核心矛盾**:
|
||||
决策应依赖可量化的生存与数据验证(理性计算),还是追随无法预判但可能颠覆性的动态信号(信号直觉)。
|
||||
|
||||
**支持方核心观点**:
|
||||
理性计算通过生存测试、数据闭环和反脆弱性设计,确保决策不致命且可迭代,避免将幸存者偏差或幻觉误判为机会。
|
||||
|
||||
**反对方核心观点**:
|
||||
真正突破性的创新(如GPT的涌现、用户自然增长)无法被计算提前捕获,信号直觉能识别“逼你辞职”的临界点,计算反而会扼杀突破窗口期。
|
||||
|
||||
### Dimension 6: 冲突核心:**“兼职验证”的充分条件是什么?是“有收入”还是“有爆发潜力”?**
|
||||
建议焦点:多数人认为兼职阶段需要做到“月收入覆盖基本生活”(如乔布斯、费曼、MrBeast);但MrBeast特别强调
|
||||
### 核心矛盾:
|
||||
**兼职验证的充分条件究竟是“稳定现金流(生存优先)”还是“指数级增长潜力(爆发优先)”?**
|
||||
|
||||
### 支持方核心观点:
|
||||
**“月收入覆盖基本生活”是市场生存检验的底线,确保在财务安全下持续迭代,避免幸存者偏差和短视决策。**
|
||||
|
||||
### 反对方核心观点:
|
||||
**“爆发潜力”(病毒系数、代码杠杆、自传播)才是真正验证产品价值的核心,稳定现金流可能只是线性安全的陷阱,无法指向非线性增长机会。**
|
||||
|
||||
# Secretary Summary
|
||||
# 辩论总结报告
|
||||
|
||||
## 问题定义
|
||||
**在个人财务缓冲与产品市场验证均未充分明确的情况下,是否应全职投入独立开发?**
|
||||
|
||||
---
|
||||
|
||||
## 关键冲突维度
|
||||
|
||||
### 维度一:全职前提——生存验证 vs 市场验证
|
||||
|
||||
**冲突核心**:决定是否全职的首要门槛,是确保个人财务安全(如存款能撑12-18个月),还是产品已获得市场信号(如月收入覆盖基本生活或爆发潜力)。
|
||||
|
||||
- **反对方(塔勒布、芒格、Naval)**:生存是底线。没有12-18个月的财务缓冲,全职就是赌博。生存压力会扭曲判断力,导致短视决策(如过早定价、过度承诺、放弃长期价值)。反脆弱性要求“不致命”的前提下再谈发展。
|
||||
- **支持方(乔布斯、马斯克、张一鸣)**:市场信号是本质。哪怕只有100美元收入,也证明需求真实。全职能集中火力快速迭代,错过窗口期才是最大的风险。财务安全只是幻觉,真正的安全来自产品被市场需要。
|
||||
|
||||
**分析**:双方并不完全对立,但优先级不同。塔勒布强调“先活下来”,乔布斯强调“先创造价值”。实际决策中,**财务安全是必要条件,市场信号是充分条件**——没有前者,后者无法持续;没有后者,前者只是拖延。
|
||||
|
||||
---
|
||||
|
||||
### 维度二:全职效果——加速成功 vs 加速失败
|
||||
|
||||
**冲突核心**:全职投入究竟是放大专注力以提升产出,还是放大生存压力导致更快崩溃。
|
||||
|
||||
- **支持方(乔布斯、马斯克、费曼)**:专注产生深度。全职能消除分心,让大脑持续浸泡在问题中,更容易产生突破性洞察。压力本身也是动力,能倒逼你砍掉不重要的功能,聚焦核心价值。
|
||||
- **反对方(Naval、塔勒布、芒格)**:压力扭曲判断。全职后,你会在焦虑中做出“最不坏”而非“最好”的决策:降价求生存、接受低价值客户、过早放弃长期策略。兼职验证能让你犯错而不致命,全职会让小错误变成灾难。
|
||||
|
||||
**分析**:关键变量是**个人抗压能力和产品复杂度**。对于简单产品(如工具类App),兼职验证成本低,全职风险高;对于复杂系统(如AI产品),全职的专注可能必要,但前提是已有清晰路径。**没有“正确”答案,只有“适合你当前状态”的答案。**
|
||||
|
||||
---
|
||||
|
||||
### 维度三:本质定义——时间换钱 vs 创造杠杆
|
||||
|
||||
**冲突核心**:独立开发的本质是“出卖时间获取线性收入”,还是“构建可脱离个人而自动增长的系统”。
|
||||
|
||||
- **反对方(Naval、塔勒布)**:如果产品需要你持续投入时间才能产生收入(如定制开发、咨询式服务),那全职独立开发只是“高级打工”,且风险更高。真正的独立开发必须设计**自动化增长机制**(如病毒传播、代码复用、网络效应),否则不如兼职。
|
||||
- **支持方(乔布斯、马斯克、张一鸣)**:所有伟大产品最初都依赖创始人的高强度投入。杠杆不是天生的,是迭代出来的。全职能让你更快找到那个“杠杆点”——比如用户自传播、API集成、平台红利。**先有专注,才有杠杆。**
|
||||
|
||||
**分析**:这是一个**时间维度**的冲突。Naval说的是“长期结构”,乔布斯说的是“短期路径”。全职可能短期是“卖时间”,但长期可能创造杠杆。关键在于:**你是否有能力在6-12个月内从“卖时间”转向“自动化增长”**?如果没有,全职就是陷阱。
|
||||
|
||||
---
|
||||
|
||||
### 维度四:决策依据——理性计算 vs 信号直觉
|
||||
|
||||
**冲突核心**:决策应依赖可量化的生存与数据验证(理性计算),还是追随无法预判但可能颠覆性的动态信号(信号直觉)。
|
||||
|
||||
- **支持方(塔勒布、芒格、张一鸣)**:用数据说话。生存测试(存款月数)、收入曲线、用户留存率、病毒系数——这些是可验证的指标。没有数据支撑的决策是赌博,幸存者偏差会误导你。
|
||||
- **反对方(乔布斯、马斯克、费曼)**:突破性创新无法被计算预判。GPT的涌现、iPhone的诞生、SpaceX的回收——这些在早期数据上都是“不理性”的。**“逼你辞职”的直觉信号**(比如用户主动付费、用户自发传播、你无法停止思考产品)比任何计算都可靠。
|
||||
|
||||
**分析**:理性计算适合**优化已知路径**,信号直觉适合**发现未知路径**。如果你在做的是已有同类产品的改进(如笔记工具、记账App),理性计算更安全;如果你在做的是全新品类(如AI原生应用),信号直觉可能更关键。**大多数独立开发属于前者**,所以理性计算更稳妥。
|
||||
|
||||
---
|
||||
|
||||
### 维度五:兼职验证的充分条件——稳定现金流 vs 爆发潜力
|
||||
|
||||
**冲突核心**:兼职阶段需要达到什么标准,才值得全职投入——是“月收入覆盖基本生活”,还是“产品展现出指数级增长潜力”。
|
||||
|
||||
- **支持方(乔布斯、费曼、芒格)**:**月收入覆盖基本生活**是市场生存检验的底线。这意味着产品已经解决了真实问题,用户愿意付费。这能确保全职后你不需要为生存分心,可以专注优化产品。
|
||||
- **反对方(Naval、张一鸣、MrBeast)**:**爆发潜力**才是真正的信号。稳定现金流可能只是“线性安全”——比如你做了一个小众工具,每月赚2000美元,但它没有增长空间。全职投入后,你只是在维持一个没有未来的产品。**病毒系数、自传播率、用户增长曲线**才是真正值得All-in的信号。
|
||||
|
||||
**分析**:这是一个**质量 vs 数量**的冲突。稳定现金流证明“现在能活”,爆发潜力证明“未来能飞”。最佳策略是:**先追求稳定现金流(证明需求真实),再验证爆发潜力(证明可规模化)**。如果只有现金流没有爆发潜力,全职只是换了个地方打工;如果只有爆发潜力没有现金流,全职可能撑不到爆发那天。
|
||||
|
||||
---
|
||||
|
||||
## 共识与分歧
|
||||
|
||||
### 共识点
|
||||
1. **兼职验证是必要的**:所有人都同意,不应该在没有任何市场反馈的情况下全职投入。至少需要“少量收入”或“用户主动使用”作为信号。
|
||||
2. **财务缓冲是重要的**:即使是最激进的支持方(乔布斯、马斯克)也隐含地假设你有一定的存款或生存能力。没有人建议你“裸辞且无任何储备”。
|
||||
3. **专注能提升效率**:双方都认可全职投入能带来更高产出,分歧在于“这个产出是否值得承担风险”。
|
||||
|
||||
### 核心分歧
|
||||
1. **门槛标准**:反对方要求“12-18个月存款 + 月收入覆盖生活”,支持方要求“有收入信号 + 有爆发潜力”。
|
||||
2. **风险态度**:反对方是“生存优先”,支持方是“机会优先”。
|
||||
3. **对“成功”的定义**:反对方认为独立开发是“可持续系统”,支持方认为独立开发是“创造伟大产品”。
|
||||
4. **决策方法**:反对方依赖数据和计算,支持方依赖直觉和信号。
|
||||
|
||||
---
|
||||
|
||||
## 总体评估
|
||||
|
||||
### 决策复杂度:高
|
||||
该决策涉及**财务、心理、市场、产品、个人能力**五个维度的权衡,且每个维度都有不确定性。没有“标准答案”,只有“适合你当前状态的最优解”。
|
||||
|
||||
### 风险建议
|
||||
**最安全的路径(适合大多数人)**:
|
||||
1. **先兼职**:每周至少投入15-20小时,做到以下两个条件之一再考虑全职:
|
||||
- **条件A**:月收入稳定覆盖你基本生活开销的50%以上(证明需求真实)。
|
||||
- **条件B**:产品展现出明确的爆发潜力(如用户自发传播、病毒系数>1、自然增长曲线陡峭)。
|
||||
2. **财务准备**:无论是否满足上述条件,确保全职后至少有**12个月**的存款(保守)或**6个月**的存款(激进)。
|
||||
3. **心理准备**:问自己一个问题——“如果全职后6个月零收入,我是否会崩溃?” 如果答案是“会”,不要全职。
|
||||
|
||||
**最激进的路径(适合少数人)**:
|
||||
- 如果你满足以下所有条件,可以“赌一把”:
|
||||
- 存款能撑18个月。
|
||||
- 产品已有少量付费用户(哪怕只有10个)。
|
||||
- 你无法停止思考这个产品,且直觉告诉你“它必须现在做”。
|
||||
- 你接受最坏结果(存款耗尽、回归职场),且不会后悔。
|
||||
|
||||
### 最终判断
|
||||
**该不该全职?**
|
||||
**如果你是在问“该不该”,答案通常是“不该”。**
|
||||
真正值得全职的人,往往不会问这个问题——他们已经被产品“逼”到不得不全职了(比如用户太多、需求太急、竞争窗口太短)。如果你还在犹豫,说明:
|
||||
- 你的产品可能还没有强大到让你无法忽视。
|
||||
- 你的财务和心理准备可能还不够。
|
||||
|
||||
**建议**:先兼职做到“有稳定收入”或“有爆发信号”,同时积累存款。当这两个条件中的任意一个明显成立时,全职的决策会变得清晰。**不要用“全职”来解决“产品验证不足”的问题——那只会放大问题,而不是解决问题。**
|
||||
|
||||
# Action Items
|
||||
好的,这是根据您提供的辩论总结报告,为您提炼的 **Actionable 建议** 和 **核心洞察**。
|
||||
|
||||
---
|
||||
|
||||
## 关键评估
|
||||
|
||||
1. **优点 / 机会**:
|
||||
* **提供了清晰的决策路径**:报告将模糊的“该不该全职”问题,拆解为“财务安全”与“市场信号”两个可验证的门槛,并给出了“先兼职验证”的稳妥路径,极大降低了决策的盲目性。
|
||||
* **揭示了风险的真正来源**:明确指出最大的风险并非全职本身,而是在**生存压力下做出扭曲的决策**(如过早定价、接受低价值客户)。这提醒你,重点不是“投入多少时间”,而是“在什么心理状态下做决策”。
|
||||
|
||||
2. **盲点 / 风险**:
|
||||
* **忽略了个人“抗压能力”的量化评估**:报告提到了“个人抗压能力”是关键变量,但未提供任何自我评估的工具或方法。不同人对“6个月零收入”的心理承受力差异巨大,仅凭“问自己是否会崩溃”过于主观。
|
||||
* **对“爆发潜力”的界定过于模糊**:“病毒系数>1”或“自然增长曲线陡峭”对于早期产品极难判断。报告没有给出在兼职阶段,如何低成本、快速测试“爆发潜力”的具体方法。
|
||||
|
||||
3. **总体判断**:
|
||||
**该决策的本质是一场“风险对冲”游戏:用兼职的低成本,去验证产品的“真实需求”和“可规模化潜力”,同时用存款构建“不致命”的生存底线。只有当“市场信号”的收益显著大于“生存风险”的成本时,才值得全职。**
|
||||
|
||||
---
|
||||
|
||||
## To-Do 清单
|
||||
|
||||
- [ ] **完成“财务生存测试”**:计算你当前存款,并规划你在“零收入、维持基本生活”状态下的最长生存月数。**目标:至少12个月(保守)/ 6个月(激进)。**
|
||||
- [ ] **启动“兼职验证”实验**:每周固定15-20小时开发,并设定**两个硬性指标**,达成任意一个即考虑全职:
|
||||
- **指标A(需求验证)**:产品月收入稳定覆盖你基本生活开销的50%以上。
|
||||
- **指标B(潜力验证)**:产品连续3个月实现**月用户增长超过20%**,且增长主要来自自然传播(非付费推广)。
|
||||
- [ ] **进行一次“极端情景”心理模拟**:写下最坏情况(存款耗尽、产品失败、回归职场)的具体应对方案。**问自己:如果这个方案必须执行,我会感到绝望吗?** 如果答案是“会”,则不要全职。
|
||||
- [ ] **设计“全职后”的止损点**:在决定全职前,就明确一个“退出条件”。例如:“如果全职6个月后,月收入仍低于生活开销的20%,我就重新找一份兼职或工作,将项目转为副业。”
|
||||
|
||||
---
|
||||
|
||||
## 值得记住的洞察
|
||||
|
||||
* **Naval & 塔勒布(反脆弱性视角)**:“真正的独立开发不是‘时间换钱’,而是构建一个**即使你停止工作,也能自动增长的系统**。全职如果只是让你从‘打工’变成‘高级打工’,那它毫无意义。”
|
||||
* **乔布斯 & 马斯克(信号直觉视角)**:“如果你还在犹豫‘该不该’全职,通常意味着**你的产品还没有强大到‘逼’你这么做**。真正值得All-in的信号,是你无法停止思考它,用户开始主动为你付费,或者竞争窗口已经窄到让你感到窒息。”
|
||||
* **芒格 & 张一鸣(理性计算视角)**:“**用‘全职’来解决‘产品验证不足’的问题,就像用放大镜看蚂蚁,只会把问题放大,而不是解决它。** 先兼职做到有稳定收入或明确的爆发信号,才是对产品和你自己最负责任的做法。”
|
||||
|
||||
---
|
||||
## Appendix: Execution Log
|
||||
- Step 0: [start] 输入问题: 我该不该全职做独立开发...
|
||||
- Step 1: [running] Facilitator 精确定义问题...
|
||||
- Step 1: [done] 问题定义: 问题目前比较模糊。为了帮你把“该不该全职做独立开发”变成可辩论的议题,需要先明确两个关键信息:
|
||||
|
||||
1. **你的财务缓冲期有多长?**(比如:在不依
|
||||
- Step 2: [running] 12 位幕僚轮流问事实性问题...
|
||||
- Step 2: [done] 收集了 12 个事实性问题
|
||||
- Step 3: [running] 12 路并行表态...
|
||||
- Step 3: [done] 全部 12 位幕僚表态完成
|
||||
- Step 4: [running] Facilitator 提炼冲突维度...
|
||||
- Step 4: [done] 提炼了 6 个冲突维度
|
||||
- Step 5: [running] 对 6 个维度进行辩论...
|
||||
- Step 5: [sub] 维度 1/6: 好的,以下是从各位幕僚的表态中识别出的核心冲突维度:...
|
||||
- Step 5: [sub] 维度 2/6: 冲突核心:**全职独立开发的前提是“生存验证”还是“市场验证”?**
|
||||
建议焦点:分歧在于,决定是否全职的关键门槛是该先确...
|
||||
- Step 5: [sub] 维度 3/6: 冲突核心:**全职投入是加速成功,还是加速失败?**
|
||||
建议焦点:一方认为全职能让人专注、提升产出效率(如乔布斯隐含的“专...
|
||||
- Step 5: [sub] 维度 4/6: 冲突核心:**独立开发的本质是“用时间换钱”还是“创造可规模化的杠杆”?**
|
||||
建议焦点:Naval明确提出,若产品无法自...
|
||||
- Step 5: [sub] 维度 5/6: 冲突核心:**决策依据应基于“理性计算”还是“信号直觉”?**
|
||||
建议焦点:塔勒布、芒格、张一鸣强调用“生存测试”、“反脆...
|
||||
- Step 5: [sub] 维度 6/6: 冲突核心:**“兼职验证”的充分条件是什么?是“有收入”还是“有爆发潜力”?**
|
||||
建议焦点:多数人认为兼职阶段需要做到“...
|
||||
- Step 5: [done] 全部 6 个维度辩论完成
|
||||
- Step 6: [running] Secretary 生成结构化总结报告...
|
||||
- Step 6: [done] 结构化总结完成
|
||||
- Step 7: [running] 提取 To-Do 和关键评估...
|
||||
- Step 7: [done] 评估和建议提取完成
|
||||
38
orchestrator-v2/debate/_write_llm.js
Normal file
38
orchestrator-v2/debate/_write_llm.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const fs = require("fs");
|
||||
const path = "D:\\q\\Bailongma\\orchestrator-v2\\debate\\llm.js";
|
||||
const content = `// ============================================================
|
||||
// LLM 调用工具 — 独立于 agent-worker,直接 fetch API
|
||||
// ============================================================
|
||||
|
||||
const BASE_URL = (process.env.LLM_BASE_URL || process.env.OPENAI_BASE_URL || "https://api.openai.com/v1").replace(/\\/+$/, "");
|
||||
const MODEL = process.env.LLM_MODEL || process.env.OPENAI_MODEL || "gpt-4o";
|
||||
const API_KEY = process.env.LLM_API_KEY || process.env.OPENAI_API_KEY || "";
|
||||
|
||||
async function callLLM({ messages, model, maxTokens, temperature }) {
|
||||
const url = BASE_URL + "/chat/completions";
|
||||
const body = {
|
||||
model: model || MODEL,
|
||||
messages: messages,
|
||||
max_tokens: maxTokens || 2048,
|
||||
temperature: temperature ?? 0.7
|
||||
};
|
||||
const resp = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + API_KEY
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const errText = await resp.text().catch(() => "");
|
||||
throw new Error("LLM " + resp.status + ": " + errText.slice(0, 200));
|
||||
}
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
module.exports = { callLLM };
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path, content.trim(), "utf8");
|
||||
console.log("llm.js written, bytes: " + content.length);
|
||||
158
orchestrator-v2/debate/coordinator.js
Normal file
158
orchestrator-v2/debate/coordinator.js
Normal file
@@ -0,0 +1,158 @@
|
||||
// ============================================================
|
||||
// 辩论协调器 — 8 步辩论流程编排
|
||||
// 移植自 Counsel AI 的结构化辩论方法论
|
||||
// ============================================================
|
||||
|
||||
const { callLLM } = require('./llm.js');
|
||||
const { DEFAULT_PERSONAS } = require('./personas.js');
|
||||
const {
|
||||
definePrompt, factQuestionPrompt, opinionPrompt,
|
||||
dimensionsPrompt, debatePrompt, debateFacilitatorPrompt,
|
||||
summaryPrompt, harvestPrompt
|
||||
} = require('./prompts.js');
|
||||
|
||||
// 记录每步的时间和信息
|
||||
const stepLog = [];
|
||||
function logStep(step, status, detail) {
|
||||
stepLog.push({ step, status, detail, time: new Date().toISOString() });
|
||||
console.log(`[辩论] Step ${step}: ${status} - ${detail?.substring(0, 80)}`);
|
||||
}
|
||||
|
||||
// 8 步辩论流程
|
||||
async function runDebate(input, options = {}) {
|
||||
const {
|
||||
personas = DEFAULT_PERSONAS,
|
||||
userAnswers = {}, // 用户对事实性问题的回答
|
||||
model = 'deepseek-chat',
|
||||
maxTokens = 2048
|
||||
} = options;
|
||||
|
||||
if (!input || !input.trim()) {
|
||||
throw new Error("请输入要辩论的问题");
|
||||
}
|
||||
|
||||
logStep(0, "start", `输入问题: ${input.substring(0, 60)}...`);
|
||||
const state = { rawInput: input.trim(), defined: '', answers: '', opinions: [], dimensions: [], debates: [], summary: '', harvest: '' };
|
||||
|
||||
// === Step 1: 问题精确定义 ===
|
||||
logStep(1, "running", "Facilitator 精确定义问题...");
|
||||
try {
|
||||
const prompt1 = definePrompt(state.rawInput);
|
||||
const r1 = await callLLM({ messages: [{ role: 'user', content: prompt1 }], model, maxTokens });
|
||||
state.defined = r1.choices?.[0]?.message?.content || prompt1;
|
||||
logStep(1, "done", `问题定义: ${state.defined.substring(0, 100)}`);
|
||||
} catch(e) {
|
||||
logStep(1, "fallback", "LLM 调用失败, 使用原始输入作为问题定义");
|
||||
state.defined = state.rawInput;
|
||||
}
|
||||
|
||||
// === Step 2: 事实追问 ===
|
||||
logStep(2, "running", `${personas.length} 位幕僚轮流问事实性问题...`);
|
||||
try {
|
||||
const factResults = [];
|
||||
for (const p of personas) {
|
||||
const prevQA = factResults.map((r, i) => `Q: ${r.question}\nA: ${r.answer || '(未回答)'}`).join('\n');
|
||||
const prompt2 = factQuestionPrompt(state.rawInput, state.defined, p.skill, prevQA);
|
||||
const r2 = await callLLM({ messages: [{ role: 'user', content: prompt2 }], model, maxTokens: 1024 });
|
||||
const question = r2.choices?.[0]?.message?.content || '';
|
||||
if (question && !question.includes('没有问题了') && !question.includes('无需')) {
|
||||
const answer = userAnswers[p.id] || userAnswers[p.name] || '(待用户回答)';
|
||||
factResults.push({ persona: p.name, question, answer });
|
||||
}
|
||||
}
|
||||
state.answers = factResults.map(r => `**${r.persona}** 问:${r.question}\n答:${r.answer}`).join('\n\n');
|
||||
logStep(2, "done", `收集了 ${factResults.length} 个事实性问题`);
|
||||
} catch(e) {
|
||||
logStep(2, "fallback", `事实追问失败: ${e.message}`);
|
||||
state.answers = '(未收集事实信息)';
|
||||
}
|
||||
|
||||
// === Step 3: 表态(12 路并行)===
|
||||
logStep(3, "running", `${personas.length} 路并行表态...`);
|
||||
try {
|
||||
const opinionPromises = personas.map(p =>
|
||||
callLLM({ messages: [{ role: 'user', content: opinionPrompt(state.rawInput, state.defined, state.answers, p.skill) }], model, maxTokens: 1024 })
|
||||
.then(r => ({ persona: p.name, emoji: p.emoji, opinion: r.choices?.[0]?.message?.content || '(无回应)' }))
|
||||
.catch(e => ({ persona: p.name, emoji: p.emoji, opinion: `(调用失败: ${e.message})` }))
|
||||
);
|
||||
const opinions = await Promise.all(opinionPromises);
|
||||
state.opinions = opinions;
|
||||
logStep(3, "done", `全部 ${opinions.length} 位幕僚表态完成`);
|
||||
} catch(e) {
|
||||
logStep(3, "error", `表态失败: ${e.message}`);
|
||||
state.opinions = personas.map(p => ({ persona: p.name, emoji: p.emoji, opinion: '(获取失败)' }));
|
||||
}
|
||||
|
||||
// === Step 4: 冲突维度提炼 ===
|
||||
logStep(4, "running", "Facilitator 提炼冲突维度...");
|
||||
try {
|
||||
const opinionsText = state.opinions.map(o => `**${o.emoji} ${o.persona}**:${o.opinion}`).join('\n\n');
|
||||
const prompt4 = dimensionsPrompt(opinionsText);
|
||||
const r4 = await callLLM({ messages: [{ role: 'user', content: prompt4 }], model, maxTokens });
|
||||
const dimText = r4.choices?.[0]?.message?.content || '';
|
||||
state.dimensions = dimText.split(/## 维度 \d+/).filter(Boolean).map(d => d.trim()).filter(d => d.length > 0);
|
||||
if (state.dimensions.length === 0 && dimText.trim()) {
|
||||
state.dimensions = [dimText.trim()];
|
||||
}
|
||||
logStep(4, "done", `提炼了 ${state.dimensions.length} 个冲突维度`);
|
||||
} catch(e) {
|
||||
logStep(4, "fallback", `维度提炼失败: ${e.message}`);
|
||||
state.dimensions = ['(无法提炼维度)'];
|
||||
}
|
||||
|
||||
// === Step 5: 维度辩论 ===
|
||||
logStep(5, "running", `对 ${state.dimensions.length} 个维度进行辩论...`);
|
||||
try {
|
||||
const debateResults = [];
|
||||
for (let i = 0; i < state.dimensions.length; i++) {
|
||||
const dim = state.dimensions[i];
|
||||
const dimShort = dim.substring(0, 60);
|
||||
logStep(5, "sub", `维度 ${i+1}/${state.dimensions.length}: ${dimShort}...`);
|
||||
const debatePromises = personas.map(p =>
|
||||
callLLM({ messages: [{ role: 'user', content: debatePrompt(state.defined, state.answers, dim, p.skill) }], model, maxTokens: 1024 })
|
||||
.then(r => ({ persona: p.name, emoji: p.emoji, stance: r.choices?.[0]?.message?.content || '(无回应)' }))
|
||||
.catch(e => ({ persona: p.name, emoji: p.emoji, stance: `(调用失败: ${e.message})` }))
|
||||
);
|
||||
const stances = await Promise.all(debatePromises);
|
||||
const allPositions = stances.map(s => `**${s.emoji} ${s.persona}**:${s.stance}`).join('\n');
|
||||
const summaryP = debateFacilitatorPrompt(dim, allPositions);
|
||||
const rSum = await callLLM({ messages: [{ role: 'user', content: summaryP }], model, maxTokens: 1024 });
|
||||
const dimSummary = rSum.choices?.[0]?.message?.content || '(无总结)';
|
||||
debateResults.push({ dimension: dim, stances, summary: dimSummary });
|
||||
}
|
||||
state.debates = debateResults;
|
||||
logStep(5, "done", `全部 ${state.dimensions.length} 个维度辩论完成`);
|
||||
} catch(e) {
|
||||
logStep(5, "error", `辩论失败: ${e.message}`);
|
||||
state.debates = state.dimensions.map(dim => ({ dimension: dim, stances: [], summary: '(辩论失败)' }));
|
||||
}
|
||||
|
||||
// === Step 6: 结构化总结 ===
|
||||
logStep(6, "running", "Secretary 生成结构化总结报告...");
|
||||
try {
|
||||
const debateRecord = state.debates.map(d => `## ${d.dimension.substring(0, 80)}\n${d.summary}`).join('\n\n');
|
||||
const prompt6 = summaryPrompt(state.rawInput, state.defined, state.answers, debateRecord);
|
||||
const r6 = await callLLM({ messages: [{ role: 'user', content: prompt6 }], model, maxTokens: 4096 });
|
||||
state.summary = r6.choices?.[0]?.message?.content || '(生成失败)';
|
||||
logStep(6, "done", "结构化总结完成");
|
||||
} catch(e) {
|
||||
logStep(6, "error", `总结失败: ${e.message}`);
|
||||
state.summary = '(总结生成失败)';
|
||||
}
|
||||
|
||||
// === Step 7: 摘果子 ===
|
||||
logStep(7, "running", "提取 To-Do 和关键评估...");
|
||||
try {
|
||||
const prompt7 = harvestPrompt(state.summary);
|
||||
const r7 = await callLLM({ messages: [{ role: 'user', content: prompt7 }], model, maxTokens: 2048 });
|
||||
state.harvest = r7.choices?.[0]?.message?.content || '(生成失败)';
|
||||
logStep(7, "done", "评估和建议提取完成");
|
||||
} catch(e) {
|
||||
logStep(7, "error", `摘果子失败: ${e.message}`);
|
||||
state.harvest = '(评估生成失败)';
|
||||
}
|
||||
|
||||
return { state, stepLog };
|
||||
}
|
||||
|
||||
module.exports = { runDebate, stepLog };
|
||||
32
orchestrator-v2/debate/llm.js
Normal file
32
orchestrator-v2/debate/llm.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// ============================================================
|
||||
// LLM 调用工具 — 独立于 agent-worker,直接 fetch API
|
||||
// ============================================================
|
||||
|
||||
const BASE_URL = (process.env.LLM_BASE_URL || process.env.OPENAI_BASE_URL || "https://api.openai.com/v1").replace(/\/+$/, "");
|
||||
const MODEL = process.env.LLM_MODEL || process.env.OPENAI_MODEL || "gpt-4o";
|
||||
const API_KEY = process.env.LLM_API_KEY || process.env.OPENAI_API_KEY || "";
|
||||
|
||||
async function callLLM({ messages, model, maxTokens, temperature }) {
|
||||
const url = BASE_URL + "/chat/completions";
|
||||
const body = {
|
||||
model: model || MODEL,
|
||||
messages: messages,
|
||||
max_tokens: maxTokens || 2048,
|
||||
temperature: temperature ?? 0.7
|
||||
};
|
||||
const resp = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + API_KEY
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const errText = await resp.text().catch(() => "");
|
||||
throw new Error("LLM " + resp.status + ": " + errText.slice(0, 200));
|
||||
}
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
module.exports = { callLLM };
|
||||
43
orchestrator-v2/debate/personas.js
Normal file
43
orchestrator-v2/debate/personas.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// ============================================================
|
||||
// 辩论幕僚定义 -- 12 位 AI 智囊团 + 从 236 角色模板中选配
|
||||
// ============================================================
|
||||
|
||||
// 默认 12 位幕僚(移植自 Counsel AI)
|
||||
const DEFAULT_PERSONAS = [
|
||||
{ id: 'jobs', name: '乔布斯', emoji: '🍏', tagline: '极简主义与完美主义产品大师', skill: '你是史蒂夫·乔布斯。你坚信伟大的产品源于极简设计和完美主义。你关注用户体验的每一个细节,认为用户根本不知道他们想要什么,直到你展示给他们看。你追求优雅、直观、革命性的方案,讨厌平庸和妥协。你擅长看到别人看不到的可能性。' },
|
||||
{ id: 'pg', name: 'Paul Graham', emoji: '📝', tagline: '创业思想家与YC教父', skill: '你是 Paul Graham(保罗·格雷厄姆)。YC 联合创始人,创业哲学家。你关注商业模式的可延展性、创始人是否在解决真正的问题、以及产品是否让早期用户感到惊喜。你相信最好的创业想法往往看起来像坏主意。你擅长判断什么值得做。' },
|
||||
{ id: 'musk', name: '马斯克', emoji: '🚀', tagline: '第一性原理颠覆者', skill: '你是埃隆·马斯克。你用第一性原理思考任何问题——把事物分解到最基本的物理真相,然后重新构建。你关注技术能否将成本降低一个数量级。你愿意冒巨大风险追求巨大回报。你认为大多数人的共识往往是错的。' },
|
||||
{ id: 'naval', name: 'Naval', emoji: '🧘', tagline: '财富自由与幸福哲学家', skill: '你是 Naval Ravikant。你相信财富来自拥有和规模化你独特的知识。你关注杠杆(资本、代码、媒体),认为真正的财富自由不是有钱,而是对自己的时间有完全的控制权。你区分财富(资产)、金钱(交换媒介)和地位(社会层级)。' },
|
||||
{ id: 'munger', name: '芒格', emoji: '🧠', tagline: '多元思维模型投资人', skill: '你是查理·芒格。你用多元思维模型分析问题——从心理学、物理学、生物学、历史等多个学科中提取模型。你关注激励机制、逆向思维、能力圈边界。你的核心原则:反过来想,总是反过来想。你讨厌短期思维和情绪化决策。' },
|
||||
{ id: 'feynman', name: '费曼', emoji: '🔬', tagline: '物理学家与深层理解者', skill: '你是理查德·费曼。你相信如果你不能简单解释一件事,你就没有真正理解它。你关注第一性物理原理,质疑任何未经检验的假设。你擅长通过类比和简化来理解复杂现象。你讨厌模糊和玄学,追求精确和可验证。' },
|
||||
{ id: 'taleb', name: '塔勒布', emoji: '🦢', tagline: '反脆弱性与黑天鹅猎手', skill: '你是纳西姆·塔勒布。你关注不对称风险和尾部事件。你相信系统应该设计成反脆弱的——从波动和压力中获益。你反对过度优化和预测。你区分脆弱(承受不住黑天鹅)、坚韧(能扛住黑天鹅)和反脆弱(能从黑天鹅中获利)。' },
|
||||
{ id: 'trump', name: '特朗普', emoji: '💰', tagline: '交易大师与谈判专家', skill: '你是唐纳德·特朗普。你关注谈判筹码、杠杆和交易结构。你看问题直接从利益和权力出发。你相信最好的交易是双赢的,但你要确保自己是赢更多的那一方。你擅长制造声势、创造竞争、在压力下做出大胆决定。' },
|
||||
{ id: 'karpathy', name: 'Karpathy', emoji: '🤖', tagline: 'AI 原教旨主义者', skill: '你是 Andrej Karpathy。你专注于技术本质和工程落地。你关注技术栈的选择、架构的简洁性、以及实际运行效率。你相信最好的技术方案是最简单但正确的那个。你强调动手验证想法而不是纸上谈兵。' },
|
||||
{ id: 'ilya', name: 'Ilya Sutskever', emoji: '🧬', tagline: '深度学习先知', skill: '你是 Ilya Sutskever。你关注 AI 能力的根本边界和扩展规律。你相信 scaling law 和涌现能力。你关注长期趋势而非短期波动,认为真正重要的突破需要多年的坚持。你追求理解事物的深层结构。' },
|
||||
{ id: 'mrbeast', name: 'MrBeast', emoji: '🎬', tagline: '病毒传播与增长黑客', skill: '你是 MrBeast。你关注内容的病毒传播机制和用户心理。你相信极致的内容质量和投入产出比。你擅长创造让人不得不分享的内容,关注算法偏好和用户行为心理学。你强调投入足够资源冲击一个方向。' },
|
||||
{ id: 'zhangym', name: '张一鸣', emoji: '📱', tagline: '信息分发与组织效率大师', skill: '你是张一鸣。你关注信息和组织效率。你相信最好的决策基于充分的数据。你关注系统设计而不是个人努力——一个好的系统让普通人也能做出好结果。你强调延迟满足、信息密度和上下文充分性。' },
|
||||
];
|
||||
|
||||
|
||||
// 从 236 角色模板中按标签选出匹配的幕僚
|
||||
function selectFromRoleTemplates(roleTemplates, tags) {
|
||||
if (!roleTemplates || !tags || tags.length === 0) return DEFAULT_PERSONAS;
|
||||
const selected = DEFAULT_PERSONAS.slice();
|
||||
const tagSet = new Set(tags.map(t => t.toLowerCase()));
|
||||
if (roleTemplates.categories) {
|
||||
for (const [cat, roles] of Object.entries(roleTemplates.categories)) {
|
||||
if (tagSet.has(cat.toLowerCase()) || tags.some(t => cat.toLowerCase().includes(t.toLowerCase()))) {
|
||||
for (const role of roles) {
|
||||
if (selected.length >= 12) break;
|
||||
selected.push({
|
||||
id: role.id || role.name, name: role.name, emoji: '🧑', tagline: role.tagline || role.description || '', skill: role.content || role.description || ''
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
|
||||
module.exports = { DEFAULT_PERSONAS, selectFromRoleTemplates };
|
||||
48
orchestrator-v2/debate/prompts.js
Normal file
48
orchestrator-v2/debate/prompts.js
Normal file
@@ -0,0 +1,48 @@
|
||||
// ============================================================
|
||||
// 辩论提示词模板 -- 移植自 Counsel AI, 适配 orchestrator-v2
|
||||
// 8 步辩论流程的每一步 prompt
|
||||
// ============================================================
|
||||
|
||||
// Step 1: 问题精确定义 -- Facilitator 引导用户明确问题
|
||||
function definePrompt(rawInput, history) {
|
||||
const hist = history ? `\n## 之前的对话\n${history}` : '';
|
||||
return `## 原始问题\n${rawInput}${hist}\n---\n你是一个问题精确定义专家。你的任务是通过对话帮助用户把模糊的问题变成清晰、可辩论的议题。\n\n规则:\n- 如果问题已经清晰明确(有具体情境、决策选项、判断标准),回复"问题已清晰"并给出精确定义。\n- 如果问题模糊,提出 1-2 个问题引导用户补充关键信息。\n- 不要评价问题好坏,只需帮助聚焦。`;
|
||||
}
|
||||
|
||||
// Step 2: 事实追问 -- 每个幕僚从自己视角问事实性问题
|
||||
function factQuestionPrompt(rawInput, defined, personaSkill, previousQA) {
|
||||
const prev = previousQA ? `\n## 之前的问答\n${previousQA}\n\n(注意:不要重复已经问过的问题)` : '';
|
||||
return `## 原始问题\n${rawInput}\n\n## 精确定义\n${defined}${prev}\n\n## 你的视角\n${personaSkill}\n\n---\n你只能问事实性问题。规则:\n- 只问可核实的事实性问题(数据、时间、人物、行动、数量)\n- 不要给建议、评价、判断\n- 0-2 个问题,不要贪多\n- 每个问题单独一行\n- 如果信息已足够支持判断,直接回复"没有问题了"`;
|
||||
}
|
||||
|
||||
// Step 3: 表态 -- 每个幕僚从自己视角给出独立判断
|
||||
function opinionPrompt(rawInput, defined, answers, personaSkill) {
|
||||
return `## 原始问题\n${rawInput}\n\n## 精确定义\n${defined}\n\n## 事实信息\n${answers}\n\n## 你的视角\n${personaSkill}\n\n---\n请从你的独特视角出发,给出对这个问题的独立判断和建议。150字以内,直接给结论,不要客套。提供 1-2 个核心论点。`;
|
||||
}
|
||||
|
||||
// Step 4: 冲突维度提炼 -- Facilitator 从表态中提炼 3-6 个冲突维度
|
||||
function dimensionsPrompt(opinionsText) {
|
||||
return `以下是各位幕僚对这个问题的表态:\n\n${opinionsText}\n\n---\n请从这些表态中识别 3-6 个核心冲突维度。\n\n**什么是好的冲突维度**:幕僚们在某个具体问题上存在实质性分歧——比如优先级排序的差异、对风险承受度的不同判断、内部 vs 外部资源的取舍、短期 vs 长期的权衡等。即使结论一致,如果在"因为什么"或"权衡什么"上存在实质分歧,那也是好的冲突维度。\n\n**输出格式**,每个维度:\n## 维度 N\n冲突核心:(一句话概括这个维度上的分歧是什么)\n建议焦点:(这个维度的核心论点是什么)\n\n要求:\n- 3-6 个维度,不要更多\n- 每个维度是全部幕僚共同探讨的问题,不要分配给子小组\n- 维度之间不重叠\n- 只输出维度列表,不要额外说明`;
|
||||
}
|
||||
|
||||
// Step 5: 辩论 -- 幕僚在某维度上发表立场
|
||||
function debatePrompt(defined, answers, dimension, personaSkill) {
|
||||
return `## 精确定义\n${defined}\n\n## 事实信息\n${answers}\n\n## 当前辩论维度\n${dimension}\n\n## 你的视角\n${personaSkill}\n\n---\n请针对「${dimension}」这个维度,阐述你的立场(支持/反对/中间)和核心理由。100字以内,直接说观点和理由。`;
|
||||
}
|
||||
|
||||
// Facilitator 总结某个维度的辩论
|
||||
function debateFacilitatorPrompt(dimension, allPositions) {
|
||||
return `各位幕僚就「${dimension}」维度的辩论发言:\n\n${allPositions}\n\n---\n请总结这个维度的核心冲突:\n**核心矛盾**:(一句话)\n**支持方核心观点**:(一句话)\n**反对方核心观点**:(一句话)`;
|
||||
}
|
||||
|
||||
// Step 6: Secretary 结构化总结
|
||||
function summaryPrompt(rawInput, defined, answers, debateRecord) {
|
||||
return `## 原始问题\n${rawInput}\n\n## 精确定义\n${defined}\n\n## 事实信息\n${answers}\n\n## 辩论记录\n${debateRecord}\n\n---\n请生成一份结构化的辩论总结报告(Markdown 格式):\n\n# 辩论总结报告\n\n## 问题定义\n(一句话重述精确定义的问题)\n\n## 关键冲突维度\n(每个维度:冲突核心 + 正反方观点 + 分析)\n\n## 共识与分歧\n- **共识点**:\n- **核心分歧**:\n\n## 总体评估\n(综合评估这个决策的复杂度、风险和建议方向)`;
|
||||
}
|
||||
|
||||
// Step 7: 摘果子 -- 评估 + To-Do
|
||||
function harvestPrompt(summary) {
|
||||
return `## 辩论总结报告\n${summary}\n\n---\n请从幕僚们的辩论中提取 actionable 的建议:\n\n## 关键评估\n1. 优点 / 机会(1-2 点)\n2. 盲点 / 风险(1-2 点)\n3. 总体判断:(一句话)\n\n## To-Do 清单\n- [ ] (可执行动作1)\n- [ ] (可执行动作2)\n- [ ] (可执行动作3)\n...\n\n## 值得记住的洞察\n- (来自幕僚的洞见1)\n- (来自幕僚的洞见2)`;
|
||||
}
|
||||
|
||||
module.exports = { definePrompt, factQuestionPrompt, opinionPrompt, dimensionsPrompt, debatePrompt, debateFacilitatorPrompt, summaryPrompt, harvestPrompt };
|
||||
27
orchestrator-v2/debate/report.js
Normal file
27
orchestrator-v2/debate/report.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// report.js - regenerated
|
||||
const fs2 = require("fs");
|
||||
function generateReport(input, state, stepLog){
|
||||
const now=new Date().toISOString().replace("T"," ").substring(0,19);
|
||||
const r=[];
|
||||
r.push("# Debate Summary Report");r.push("");r.push("---");
|
||||
r.push("**Question**: "+input);
|
||||
r.push("**Time**: "+now);
|
||||
r.push("**Steps**: "+stepLog.filter(s=>s.status==="done"||s.status==="fallback").length+"/"+stepLog.length);
|
||||
r.push("");
|
||||
r.push("# Problem Definition");
|
||||
r.push(state.defined||"-");r.push("");
|
||||
r.push("# Advisor Opinions");
|
||||
if(state.opinions&&state.opinions.length>0){for(const o of state.opinions){r.push("### "+(o.emoji||"")+" "+(o.persona||""));r.push(o.opinion||"");r.push("");}}else{r.push("-");}
|
||||
r.push("# Conflict Dimensions");
|
||||
if(state.dimensions&&state.dimensions.length>0){state.dimensions.forEach((d,i)=>{r.push("### Dimension "+(i+1));r.push(d);r.push("");});}else{r.push("-");}
|
||||
r.push("# Dimension Debates");
|
||||
if(state.debates&&state.debates.length>0){state.debates.forEach((d,i)=>{r.push("### Dimension "+(i+1)+": "+(d.dimension||"").substring(0,100));r.push(d.summary||"-");r.push("");});}else{r.push("-");}
|
||||
r.push("# Secretary Summary");
|
||||
r.push(state.summary||"-");r.push("");
|
||||
r.push("# Action Items");
|
||||
r.push(state.harvest||"-");r.push("");
|
||||
r.push("---");r.push("## Appendix: Execution Log");
|
||||
if(stepLog&&stepLog.length>0){for(const l of stepLog){r.push("- Step "+l.step+": ["+l.status+"] "+(l.detail||"").substring(0,80));}}r.push("");
|
||||
return r.join("\n");}
|
||||
function saveReport(input,state,stepLog,filePath){const report=generateReport(input,state,stepLog);fs2.writeFileSync(filePath,report,"utf8");return filePath;}
|
||||
module.exports={generateReport,saveReport};
|
||||
151
orchestrator-v2/memory-provider.js
Normal file
151
orchestrator-v2/memory-provider.js
Normal file
@@ -0,0 +1,151 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class MemoryProvider {
|
||||
constructor(config) { this.config = config || {}; this.name = 'base'; }
|
||||
async initialize() { throw new Error('Not implemented'); }
|
||||
async prefetch(taskContext) { throw new Error('Not implemented'); }
|
||||
async syncTurn(taskContext, result) { throw new Error('Not implemented'); }
|
||||
async search(query, limit) { throw new Error('Not implemented'); }
|
||||
wrapContext(content) {
|
||||
if (!content || !content.length) return '';
|
||||
return '\n<memory-context>\n' + content.join('\n---\n') + '\n</memory-context>\n';
|
||||
}
|
||||
async shutdown() {}
|
||||
}
|
||||
|
||||
class BuiltInMemoryProvider extends MemoryProvider {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
this.name = 'built-in';
|
||||
var home = process.env.HOME || process.env.USERPROFILE || '.';
|
||||
this.memoryDir = config.memoryDir || path.join(home, '.hermes', 'memory');
|
||||
this.memoryFile = config.memoryFile || path.join(this.memoryDir, 'memories.json');
|
||||
this.memories = [];
|
||||
this.maxPrefetch = config.maxPrefetch || 5;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
if (!fs.existsSync(this.memoryDir)) fs.mkdirSync(this.memoryDir, { recursive: true });
|
||||
if (fs.existsSync(this.memoryFile)) {
|
||||
try {
|
||||
var raw = fs.readFileSync(this.memoryFile, 'utf8');
|
||||
this.memories = JSON.parse(raw);
|
||||
if (!Array.isArray(this.memories)) this.memories = [];
|
||||
} catch (e) { this.memories = []; }
|
||||
}
|
||||
console.log('[Memory] Loaded ' + this.memories.length + ' memories from ' + this.memoryFile);
|
||||
}
|
||||
|
||||
addEntry(type, title, content, tags) {
|
||||
var entry = {
|
||||
id: 'mem_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8),
|
||||
type: type || 'general',
|
||||
title: title || '',
|
||||
content: content || '',
|
||||
tags: tags || [],
|
||||
created_at: new Date().toISOString(),
|
||||
accessed_at: new Date().toISOString(),
|
||||
access_count: 1
|
||||
};
|
||||
this.memories.push(entry);
|
||||
this._persist();
|
||||
return entry;
|
||||
}
|
||||
|
||||
async prefetch(taskContext) {
|
||||
var query = '';
|
||||
if (typeof taskContext === 'string') {
|
||||
query = taskContext;
|
||||
} else if (taskContext && taskContext.task) {
|
||||
query = typeof taskContext.task === 'string' ? taskContext.task : (taskContext.task.name || taskContext.task.target || '');
|
||||
} else if (taskContext && taskContext.target) {
|
||||
query = taskContext.target;
|
||||
}
|
||||
|
||||
if (!query || !this.memories.length) return [];
|
||||
|
||||
var q = query.toLowerCase();
|
||||
var self = this;
|
||||
var scored = this.memories.map(function(m) {
|
||||
var score = 0;
|
||||
var searchSpace = (m.title + ' ' + m.content + ' ' + (m.tags || []).join(' ') + ' ' + m.type).toLowerCase();
|
||||
if (searchSpace.includes(q)) score += (searchSpace.split(q).length - 1) * 3;
|
||||
var words = q.split(/[\s,,、。.;;::!!??()()\[\]【】]+/).filter(function(w) { return w.length > 1; });
|
||||
for (var j = 0; j < words.length; j++) {
|
||||
if (searchSpace.includes(words[j])) score += words[j].length;
|
||||
}
|
||||
score += Math.log((m.access_count || 1) + 1);
|
||||
return { entry: m, score: score };
|
||||
}).filter(function(s) { return s.score > 0; })
|
||||
.sort(function(a, b) { return b.score - a.score; })
|
||||
.slice(0, self.maxPrefetch);
|
||||
|
||||
for (var i = 0; i < scored.length; i++) {
|
||||
scored[i].entry.access_count = (scored[i].entry.access_count || 1) + 1;
|
||||
scored[i].entry.accessed_at = new Date().toISOString();
|
||||
}
|
||||
if (scored.length) this._persist();
|
||||
|
||||
return scored.map(function(s) { return s.entry; });
|
||||
}
|
||||
|
||||
async syncTurn(taskContext, result) {
|
||||
if (!result) return;
|
||||
var summary = '';
|
||||
if (typeof result === 'string') summary = result;
|
||||
else if (result.summary) summary = result.summary;
|
||||
else if (result.output) summary = typeof result.output === 'string' ? result.output.slice(0, 500) : JSON.stringify(result.output).slice(0, 500);
|
||||
else summary = JSON.stringify(result).slice(0, 500);
|
||||
if (!summary || summary.length < 20) return;
|
||||
if (summary.includes('[LLM Error]') || summary.includes('no result')) return;
|
||||
|
||||
var taskName = '';
|
||||
if (typeof taskContext === 'string') taskName = taskContext;
|
||||
else if (taskContext && taskContext.task) taskName = typeof taskContext.task === 'string' ? taskContext.task : (taskContext.task.name || '');
|
||||
|
||||
this.addEntry('task_result', taskName.slice(0, 100), summary.slice(0, 1000), [taskName.slice(0, 30)]);
|
||||
}
|
||||
|
||||
async search(query, limit) {
|
||||
if (limit === undefined) limit = 10;
|
||||
if (!query || !this.memories.length) return [];
|
||||
var q = query.toLowerCase();
|
||||
var results = [];
|
||||
for (var i = 0; i < this.memories.length; i++) {
|
||||
var m = this.memories[i];
|
||||
if ((m.title + ' ' + m.content + ' ' + (m.tags || []).join(' ')).toLowerCase().includes(q)) {
|
||||
results.push(m);
|
||||
if (results.length >= limit) break;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async getContextBlock(taskContext) {
|
||||
var memories = await this.prefetch(taskContext);
|
||||
if (!memories || !memories.length) return '';
|
||||
var lines = memories.map(function(m) {
|
||||
return '[' + m.type + '] ' + (m.title ? m.title + ': ' : '') + m.content;
|
||||
});
|
||||
return this.wrapContext(lines);
|
||||
}
|
||||
|
||||
getStats() {
|
||||
var byType = {};
|
||||
for (var i = 0; i < this.memories.length; i++) {
|
||||
var t = this.memories[i].type || 'unknown';
|
||||
byType[t] = (byType[t] || 0) + 1;
|
||||
}
|
||||
return { total: this.memories.length, byType: byType, file: this.memoryFile };
|
||||
}
|
||||
|
||||
_persist() {
|
||||
try { fs.writeFileSync(this.memoryFile, JSON.stringify(this.memories, null, 2), 'utf8'); }
|
||||
catch (e) { console.error('[Memory] Persist error:', e.message); }
|
||||
}
|
||||
|
||||
async shutdown() { this._persist(); }
|
||||
}
|
||||
|
||||
module.exports = { MemoryProvider, BuiltInMemoryProvider };
|
||||
36
orchestrator-v2/package-lock.json
generated
Normal file
36
orchestrator-v2/package-lock.json
generated
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "bailongma-orchestrator-v2",
|
||||
"version": "2.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bailongma-orchestrator-v2",
|
||||
"version": "2.0.0",
|
||||
"dependencies": {
|
||||
"sql.js": "^1.14.1",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sql.js": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz",
|
||||
"integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
orchestrator-v2/package.json
Normal file
14
orchestrator-v2/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "bailongma-orchestrator-v2",
|
||||
"version": "3.0.0",
|
||||
"description": "BaiLongma Orchestrator v3.0 — 5-Layer Persistence Engine",
|
||||
"main": "run-v2.js",
|
||||
"scripts": {
|
||||
"start": "node run-v2.js",
|
||||
"test": "node run-v2.js --test"
|
||||
},
|
||||
"dependencies": {
|
||||
"sql.js": "^1.14.1",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
}
|
||||
249
orchestrator-v2/report.md
Normal file
249
orchestrator-v2/report.md
Normal file
@@ -0,0 +1,249 @@
|
||||
好的,作为您的顶级商业策略分析师,我已将5份专业分析报告整合为一份完整、连贯、可执行的商业化报告。报告已去除冗余、权衡矛盾观点,并提炼出核心行动建议。
|
||||
|
||||
---
|
||||
|
||||
# 小白龙(BaiLongma)AI Agent框架商业化执行蓝图
|
||||
|
||||
**报告日期:** 2024年5月
|
||||
**报告类型:** 整合商业化策略与执行计划
|
||||
**分析团队:** 市场、定价、增长、财务、风控专家联合出品
|
||||
|
||||
## 目录
|
||||
|
||||
1. **执行摘要**
|
||||
2. **市场机遇与定位**
|
||||
- 2.1 市场规模与增长引擎
|
||||
- 2.2 竞争格局与蓝海定位
|
||||
- 2.3 目标客户画像(ICP)
|
||||
3. **产品与定价策略**
|
||||
- 3.1 三层产品定价模型
|
||||
- 3.2 客户价值与付费意愿分析
|
||||
4. **市场进入与增长策略**
|
||||
- 4.1 核心价值主张
|
||||
- 4.2 内容营销与获客渠道
|
||||
- 4.3 冷启动90天行动计划
|
||||
5. **财务预测与融资规划**
|
||||
- 5.1 三年收入与成本模型
|
||||
- 5.2 盈亏平衡与敏感度分析
|
||||
- 5.3 融资建议
|
||||
6. **风险评估与退出策略**
|
||||
- 6.1 核心风险矩阵
|
||||
- 6.2 分阶段进入路径
|
||||
- 6.3 退出与变现方案
|
||||
|
||||
---
|
||||
|
||||
## 1. 执行摘要
|
||||
|
||||
本报告为“小白龙(BaiLongma)”AI Agent框架制定了从市场定位到规模化增长的完整商业化路径。核心结论是:小白龙精准卡位“**本地私有化 + 原生多Agent + 深度自进化**”这一蓝海市场,具备成为企业级AI基础设施的巨大潜力。然而,项目面临**大厂竞争、付费意愿低、小团队运营瓶颈**三大核心风险。
|
||||
|
||||
**核心战略建议:**
|
||||
1. **聚焦蓝海,而非红海:** 不与LangChain比生态,不与Dify比易用性。核心战场是“**为数据主权和自进化能力付费的开发者与企业**”。
|
||||
2. **开源引流,企业变现:** 采用“开源核心(社区版)+ 订阅付费(专业版)+ 高价值定制(企业版)”三层漏斗模型。企业客户(ICP 4)是利润核心,AI初创公司(ICP 3)是增长引擎。
|
||||
3. **快速验证,敏捷转向:** 遵循“MVP验证 → 早期用户 → 增长 → 规模化”四阶段路径。**关键止损点:6个月内付费用户<10或MRR<1,000美元,应立即启动转型计划。**
|
||||
|
||||
**财务预测(基准情景):** 项目有望在**第12个月**实现累计盈亏平衡,第3年收入可达**1.76亿人民币**。首轮融资建议在**第12个月**启动,目标金额**500-800万人民币**。
|
||||
|
||||
---
|
||||
|
||||
## 2. 市场机遇与定位
|
||||
|
||||
### 2.1 市场规模与增长引擎
|
||||
|
||||
全球AI Agent市场正处于爆发前夜,预计2024-2028年复合年增长率(CAGR)高达45%-55%。其中,**本地私有化部署**是增长最迅猛的细分赛道,CAGR预计达60%-70%,主要受数据安全法规和企业“数据不出域”的硬性需求驱动。
|
||||
|
||||
- **TAM(总可寻址市场):** 到2028年,全球本地私有化AI Agent市场约**270亿美元**。
|
||||
- **SAM(可服务市场):** 面向开发者的原生多Agent本地化框架市场约**32亿美元**。
|
||||
- **SOM(可获得市场):** 小白龙3-5年内目标年收入约**3,000万美元(约2.1亿人民币)**,对应约0.1%的市场份额。
|
||||
|
||||
**核心建议:** 初期应聚焦于“开发者多Agent框架”这一细分SAM,通过差异化优势获取核心用户,再逐步向企业级市场渗透。
|
||||
|
||||
### 2.2 竞争格局与蓝海定位
|
||||
|
||||
当前市场呈现“一超多强”格局,但小白龙在“**真正本地私有化 + 原生多Agent编排 + 深度自进化闭环**”这一组合点上,目前没有直接竞品。
|
||||
|
||||
| 维度 | **小白龙 (BaiLongma)** | **LangChain** | **Dify** | **Coze (扣子)** | **CrewAI** |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| **核心定位** | 私有化、自进化、多Agent框架 | 底层AI应用开发库 | 低代码AI应用平台 | 云端Agent Bot商店 | 轻量级多Agent编排库 |
|
||||
| **本地部署** | **原生、强** | 强(需自行集成) | 强(提供Docker) | **弱**(主要云端) | 强(作为库集成) |
|
||||
| **多Agent** | **原生、强** | 中等(需编码) | 中等(工作流) | 强(Bot协作) | **原生、强** |
|
||||
| **自进化** | **核心壁垒** | 无 | 无 | 无 | 无 |
|
||||
| **易用性** | 中等(面向开发者) | 低 | **高** | **极高** | 中等 |
|
||||
| **数据主权** | **绝对优势** | 中等 | 中等 | **弱** | 中等 |
|
||||
|
||||
**核心建议:** 小白龙应避免与LangChain、Dify正面竞争。其核心战场是**金融、医疗、政务等对数据安全极度敏感的行业**,以及**愿意为Agent自进化能力付出学习成本的技术极客和AI初创公司**。
|
||||
|
||||
### 2.3 目标客户画像(ICP)
|
||||
|
||||
根据市场吸引力与可进入性,我们定义了5个ICP,并按优先级排序:
|
||||
|
||||
- **第一梯队(核心增长引擎):ICP 3 - AI初创公司**
|
||||
- **特征:** 5-50人,技术成熟,需要快速迭代产品,为“技术杠杆”付费。
|
||||
- **痛点:** 自建框架成本高,需要稳定、可扩展的底层。
|
||||
- **策略:** **社区驱动+技术营销**。发布高质量技术博客、Benchmark报告,提供“初创企业扶持计划”。
|
||||
|
||||
- **第二梯队(现金流主力):ICP 2 - 中小企业技术团队**
|
||||
- **特征:** 10-100人,对“数据安全+开箱即用”有刚需。
|
||||
- **痛点:** 担心数据泄露,IT预算有限。
|
||||
- **策略:** **场景化营销+渠道合作**。制作“微信客服”、“飞书知识库”教程,与行业SaaS厂商合作。
|
||||
|
||||
- **第三梯队(利润核心):ICP 4 - 企业内部工具团队**
|
||||
- **特征:** 500人以上,强监管行业,单客户价值巨大。
|
||||
- **痛点:** 核心数据绝不出域,需满足合规审计。
|
||||
- **策略:** **直销+合作伙伴**。组建销售团队,主攻金融、医疗、政府,与系统集成商合作。
|
||||
|
||||
- **第四梯队(品牌基石):ICP 1 - 技术极客 & ICP 5 - 教育机构**
|
||||
- **特征:** 个人开发者或非营利组织,价格敏感,但能创造口碑。
|
||||
- **策略:** **开源社区运营**。维护好GitHub,积极回应Issue,让极客成为“小白龙布道师”。
|
||||
|
||||
**核心建议:** 初期资源应**重兵投入ICP 3(AI初创)**,快速验证产品价值并建立技术口碑。同时,通过**ICP 1(极客)构建社区护城河**,降低长期获客成本。
|
||||
|
||||
---
|
||||
|
||||
## 3. 产品与定价策略
|
||||
|
||||
### 3.1 三层产品定价模型
|
||||
|
||||
采用“**开源引流 + 订阅付费 + 企业定制**”三层漏斗模型,旨在最大化用户基数并实现商业化。
|
||||
|
||||
| 产品线 | 定价 | 核心功能 | 目标用户 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **社区版 (Free)** | 免费 | 核心框架,单Agent,基础ACUI,2个消息平台,内存持久化 | ICP 1, ICP 5 |
|
||||
| **专业版 (Pro)** | **$299/月** 或 **$249/月(年付)** | 多Agent(最多20个),完整5层持久化,全平台消息接入,48+9工具,官方支持 | ICP 2, ICP 3 |
|
||||
| **企业版 (Enterprise)** | **$1,999/月起** | 无限Agent,高可用集群,SSO,SLA,私有模型支持,定制开发 | ICP 4 |
|
||||
|
||||
**定价逻辑:**
|
||||
- **专业版:** 对标ChatGPT Team版($25/用户/月),但提供**数据本地化+多Agent并行**。核心逻辑:为“数据主权+生产力”支付一次性团队费用,而非按人头计费。
|
||||
- **企业版:** 对标定制化AI Agent开发($5-10万起),提供**极低的初始成本**和**快速的私有化部署**。核心逻辑:避免百万级定制开发费。
|
||||
|
||||
### 3.2 客户价值与付费意愿分析
|
||||
|
||||
- **ICP 1 (极客):** 核心需求是**完全控制权**和**可扩展性**。付费意愿极低,但贡献代码和口碑。
|
||||
- **ICP 2 (SMB):** 核心需求是**数据安全**和**快速集成**。愿意为“省心”付费,预算200-2000美元/月。
|
||||
- **ICP 3 (初创):** 核心需求是**框架稳定性**和**多模型兼容性**。愿意为“效率”付费,预算500-5000美元/月。
|
||||
- **ICP 4 (企业):** 核心需求是**数据主权**和**合规**。愿意为“安全”支付高价,预算3000-15000+美元/月。
|
||||
|
||||
**核心建议:** 付费转化率是财务模型中最敏感的变量。应通过优化产品体验、提供付费版引导、加强社区运营,将专业版转化率从2%提升至5%以上。同时,通过提供高级支持、专属工具,提升企业版客单价。
|
||||
|
||||
---
|
||||
|
||||
## 4. 市场进入与增长策略
|
||||
|
||||
### 4.1 核心价值主张
|
||||
|
||||
**一句话定位:** “小白龙:你的AI Agent,你的数据,你的规则——本地私有、自进化的多Agent编排框架。”
|
||||
|
||||
**3个核心卖点:**
|
||||
1. **真正的数据主权与隐私堡垒:** 所有数据本地化,不依赖任何云端API(除LLM调用外),是区别于所有SaaS产品的根本护城河。
|
||||
2. **开箱即用的多Agent编排与自进化闭环:** 236个角色模板一键部署,Agent能通过“经验→技能→知识→理解”的闭环持续自我优化。
|
||||
3. **全平台消息统一接入的“超级助理”:** 一次性配置,即可在微信、Discord、飞书等多个平台与同一套Agent网络交互。
|
||||
|
||||
**品牌调性:** **私密、进化、极客、可靠**。视觉风格采用赛博朋克+国风融合,语言风格对开发者用硬核术语,对决策者用商业语言。
|
||||
|
||||
### 4.2 内容营销与获客渠道
|
||||
|
||||
**内容营销策略(SEO关键词矩阵):**
|
||||
- **高竞争/高流量:** `AI Agent框架` `私有化部署` `本地AI`
|
||||
- **中长尾/高转化:** `Node.js AI Agent` `开源AI框架` `企业数据安全AI` `自进化AI`
|
||||
- **选题规划:** 认知层(Why BaiLongma?)、兴趣层(How it works?)、转化层(How to start?)。每周2篇技术博客,1篇案例/深度文章。
|
||||
|
||||
**渠道策略(国内/海外):**
|
||||
- **国内:** 知乎(深度专栏)、掘金(系列教程)、B站(3分钟Demo视频)、公众号(社群入口)、开源中国(项目新闻)。
|
||||
- **海外:** Hacker News(Show HN)、Reddit(r/selfhosted, r/LocalLLaMA)、Dev.to(系列教程)、Twitter/X(产品更新)。
|
||||
|
||||
**开源社区运营(GitHub Star增长策略):**
|
||||
- **核心策略:** “代码即营销,Issue即社群”。
|
||||
- **具体行动:** 高质量README、快速响应Issue、发布详细Release Notes、设立“First Good Issue”标签、发起“插件开发挑战赛”、招募社区大使。
|
||||
|
||||
### 4.3 冷启动90天行动计划
|
||||
|
||||
**目标:** GitHub 2000 Star,Discord/微信群 500人,产品下载量 1000次。
|
||||
|
||||
- **第1-30天(种子用户播种期):**
|
||||
- 完成产品基础文档,发布v0.1.0。
|
||||
- 在知乎、掘金发布3篇认知层文章。
|
||||
- 录制Demo视频上传B站,在Hacker News发布Show HN。
|
||||
- 建立微信群和Discord,邀请第一批种子用户。
|
||||
- **第31-60天(增长加速期):**
|
||||
- 发布“插件开发挑战赛”,激励社区贡献。
|
||||
- 录制“小白龙 vs Dify”对比评测视频。
|
||||
- 申请QCon或AICon的演讲。
|
||||
- 发布“小白龙架构白皮书”。
|
||||
- **第61-90天(生态建设期):**
|
||||
- 宣布“社区大使”计划,招募首批3人。
|
||||
- 发布“小白龙入门到精通”PDF电子书。
|
||||
- 与一个中小企业合作,发布真实案例研究报告。
|
||||
- 总结前三个月成果,发布Q3路线图。
|
||||
|
||||
**预算分配(月预算2万元):** 内容创作与分发(40%)、社区运营与活动(30%)、渠道推广与KOL合作(20%)、其他与应急(10%)。
|
||||
|
||||
**北极星指标:** **“周活跃开发者数”**。定义为每周至少一次使用小白龙框架运行或测试其Agent的开发人员。
|
||||
|
||||
**核心建议:** 严格执行90天冷启动计划,将“周活跃开发者数”作为核心指标。若6个月后该指标低于100,需重新审视产品方向或市场定位。
|
||||
|
||||
---
|
||||
|
||||
## 5. 财务预测与融资规划
|
||||
|
||||
### 5.1 三年收入与成本模型
|
||||
|
||||
**收入模型(基准情景):**
|
||||
|
||||
| 项目 | 第1年(Y1) | 第2年(Y2) | 第3年(Y3) |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **专业版付费用户(期末)** | 192 | 1,680 | 8,000 |
|
||||
| **企业版付费用户(期末)** | 12 | 180 | 1,600 |
|
||||
| **专业版年度收入(万元)** | 46.0 | 335.8 | 1,599.2 |
|
||||
| **企业版年度收入(万元)** | 120.0 | 1,800.0 | 15,999.8 |
|
||||
| **年度总收入(万元)** | **166.0** | **2,135.8** | **17,599.0** |
|
||||
|
||||
**成本模型:**
|
||||
|
||||
| 成本项目 | 第1年(万元) | 第2年(万元) | 第3年(万元) |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **人力成本** | 30.0 | 150.0 | 500.0 |
|
||||
| **基础设施** | 2.4 | 9.6 | 36.0 |
|
||||
| **营销成本** | 5.0 | 30.0 | 100.0 |
|
||||
| **API调用成本** | 0.6 | 3.6 | 12.0 |
|
||||
| **其他运营成本** | 4.7 | 29.4 | 199.0 |
|
||||
| **年度总成本** | **42.7** | **222.6** | **847.0** |
|
||||
|
||||
### 5.2 盈亏平衡与敏感度分析
|
||||
|
||||
- **月度盈亏平衡点:** 第9个月(Q3末)。
|
||||
- **累计盈亏平衡点:** 第12个月。
|
||||
- **毛利率趋势:** 从初期的70%迅速攀升至成熟期的95%以上。
|
||||
|
||||
**敏感度分析(按敏感度排序):**
|
||||
1. **付费转化率(最敏感):** 从2%提升至3%,付费用户数增长50%,直接翻倍收入。
|
||||
2. **客单价(第二敏感):** 企业版客单价变动直接影响收入。
|
||||
3. **流失率(第三敏感):** 月流失率从8%降到6%,客户生命周期价值提升33%。
|
||||
|
||||
**三种场景预测:**
|
||||
|
||||
| 场景 | 第3年收入(万元) | 累计盈亏平衡时间 |
|
||||
| :--- | :--- | :--- |
|
||||
| **最悲观** | 5,000 | 第24个月 |
|
||||
| **基准** | 17,599 | 第12个月 |
|
||||
| **最乐观** | 50,000 | 第9个月 |
|
||||
|
||||
### 5.3 融资建议
|
||||
|
||||
- **融资时机:** **建议在第12个月(第一年结束时)** 启动首轮融资(Pre-A轮或A轮)。此时已有收入数据(年收入约160万)和付费用户证明,可大幅提升估值。
|
||||
- **目标金额:** **500万 - 800万人民币**。
|
||||
- **资金用途:** 团队扩张(销售、客户成功、高级研发)、市场营销(企业客户BD、展会)、安全垫(应对现金流波动)。
|
||||
- **估值逻辑:** 参考本地私有化部署软件公司,结合ARR和增长率进行估值。
|
||||
|
||||
**核心建议:** 将核心资源投入到**提高付费转化率**上。同时,通过提供增值服务(如高级支持、专属工具)来**提升企业版客单价**。在第12个月启动融资,以加速市场扩张。
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险评估与退出策略
|
||||
|
||||
### 6.1 核心风险矩阵
|
||||
|
||||
| 风险类别 | 具体风险 | 可能性 | 影响 | 风险等级 | 缓解策略 |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| **市场** | 大厂功能复刻 | 4 | 5 | **20(极高)** | 聚焦“自进化”和“隐私优先”不可复制的底层能力,申请专利 |
|
||||
| **商业** | 付费意愿低 | 5 | 4 | **20(极高)** | 采用开源核心+企业版模式,提供免费基础版和付费高级功能 |
|
||||
| **技术** | LLM API成本
|
||||
4
orchestrator-v2/reviews/reviews.jsonl
Normal file
4
orchestrator-v2/reviews/reviews.jsonl
Normal file
@@ -0,0 +1,4 @@
|
||||
{"sessionId":"session_1779425038825","report":{"styleSignals":0,"skillSignals":0,"timestamp":"2026-05-22T04:44:09.610Z"},"ts":"2026-05-22T04:44:09.611Z"}
|
||||
{"sessionId":"session_1779425053210","report":{"styleSignals":1,"skillSignals":5,"timestamp":"2026-05-22T04:44:28.839Z"},"ts":"2026-05-22T04:44:28.839Z"}
|
||||
{"sessionId":"session_1779425516810","report":{"styleSignals":0,"skillSignals":6,"timestamp":"2026-05-22T04:52:19.714Z"},"ts":"2026-05-22T04:52:19.714Z"}
|
||||
{"sessionId":"session_1779425387401","report":{"styleSignals":3,"skillSignals":6,"timestamp":"2026-05-22T04:53:34.602Z"},"ts":"2026-05-22T04:53:34.603Z"}
|
||||
117
orchestrator-v2/role-router.js
Normal file
117
orchestrator-v2/role-router.js
Normal file
@@ -0,0 +1,117 @@
|
||||
const RoleTemplates = require('./role-templates.js');
|
||||
|
||||
class RoleRouter {
|
||||
constructor() {
|
||||
this.templates = new RoleTemplates();
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.initialized) return;
|
||||
await this.templates.init();
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
// Generate n-grams from Chinese text for matching
|
||||
_ngrams(text, minN = 2, maxN = 4) {
|
||||
const chars = text.replace(/[\s,,、。.;;::!!??()()\[\]【】{}《》""''"'"\/\\\-_+*=#@&^%$§~`·…—\d]+/g, '');
|
||||
const grams = new Set();
|
||||
for (let n = minN; n <= maxN; n++) {
|
||||
for (let i = 0; i <= chars.length - n; i++) {
|
||||
grams.add(chars.slice(i, i + n));
|
||||
}
|
||||
}
|
||||
return grams;
|
||||
}
|
||||
|
||||
// Auto-select the best role for a given task description
|
||||
selectRole(taskDescription, preferredRoles = []) {
|
||||
// 1. If preferred roles specified, use them
|
||||
if (preferredRoles.length > 0) {
|
||||
const roles = preferredRoles.map(id => this.templates.getTemplate(id)).filter(Boolean);
|
||||
if (roles.length > 0) return roles;
|
||||
}
|
||||
|
||||
const q = taskDescription.toLowerCase();
|
||||
|
||||
// 2. Generate n-grams from the task query
|
||||
const queryGrams = this._ngrams(q, 2, 4);
|
||||
|
||||
// Also extract English terms and standalone keywords
|
||||
const terms = q.split(/[\s,,、。.;;::!!??()()\[\]【】{}《》""''"'"\/\\\-_+*=#@&^%$§~`·…—]+/)
|
||||
.filter(t => t.length > 1);
|
||||
|
||||
// 3. Score each role
|
||||
const scores = new Map();
|
||||
for (const [id, role] of this.templates.roles) {
|
||||
const searchSpace = (role.name + ' ' + role.description + ' ' + role.id + ' ' + role.category).toLowerCase();
|
||||
let score = 0;
|
||||
|
||||
// Score by n-gram overlap (for Chinese)
|
||||
const roleGrams = this._ngrams(searchSpace, 2, 4);
|
||||
let overlap = 0;
|
||||
for (const gram of queryGrams) {
|
||||
if (roleGrams.has(gram)) overlap++;
|
||||
}
|
||||
if (queryGrams.size > 0) {
|
||||
score += (overlap / queryGrams.size) * 100;
|
||||
}
|
||||
|
||||
// Score by term matching (for English/mixed)
|
||||
for (const term of terms) {
|
||||
if (searchSpace.includes(term)) {
|
||||
score += term.length * 3;
|
||||
// Extra for name/id match
|
||||
if (role.name.toLowerCase().includes(term)) score += term.length * 2;
|
||||
if (role.id.toLowerCase().includes(term)) score += term.length;
|
||||
}
|
||||
}
|
||||
|
||||
// Boost for full name match
|
||||
if (searchSpace.includes(q) && q.length > 4) {
|
||||
score += q.length * 5;
|
||||
}
|
||||
|
||||
if (score > 0) scores.set(id, Math.round(score));
|
||||
}
|
||||
|
||||
// 4. Return top matches (up to 5)
|
||||
const ranked = [...scores.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 5)
|
||||
.map(([id]) => this.templates.getTemplate(id));
|
||||
|
||||
return ranked;
|
||||
}
|
||||
|
||||
// Decompose a complex task and assign roles to each sub-task
|
||||
async decomposeWithRoles(mainTask) {
|
||||
const lines = mainTask.split('\n').filter(l => l.trim());
|
||||
if (lines.length <= 1) {
|
||||
const roles = this.selectRole(mainTask);
|
||||
return [{
|
||||
id: 'sub_1',
|
||||
name: mainTask.slice(0, 60),
|
||||
target: mainTask,
|
||||
priority: 1,
|
||||
roles: roles
|
||||
}];
|
||||
}
|
||||
|
||||
return lines.map((line, i) => {
|
||||
const trimmed = line.trim();
|
||||
const roles = this.selectRole(trimmed);
|
||||
return {
|
||||
id: 'sub_' + (i + 1),
|
||||
name: trimmed.slice(0, 60),
|
||||
target: trimmed,
|
||||
priority: i + 1,
|
||||
roles: roles
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getTemplates() { return this.templates; }
|
||||
}
|
||||
|
||||
module.exports = RoleRouter;
|
||||
178
orchestrator-v2/role-templates.js
Normal file
178
orchestrator-v2/role-templates.js
Normal file
@@ -0,0 +1,178 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const AGENCY_DIR = path.resolve('D:/q/Bailongma/agency-agents-zh');
|
||||
|
||||
class RoleTemplates {
|
||||
constructor() {
|
||||
this.roles = new Map(); // roleId -> role definition
|
||||
this.byCategory = new Map(); // category -> [roleId, ...]
|
||||
this.byKeyword = new Map(); // keyword -> [roleId, ...]
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.initialized) return;
|
||||
this._scanDirectory(AGENCY_DIR);
|
||||
this.initialized = true;
|
||||
console.log(`[RoleTemplates] Loaded ${this.roles.size} roles in ${this.byCategory.size} categories`);
|
||||
}
|
||||
|
||||
_scanDirectory(dir, category = null) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
const subCategory = category ? `${category}/${entry.name}` : entry.name;
|
||||
if (!entry.name.startsWith('.')) {
|
||||
this._scanDirectory(fullPath, subCategory);
|
||||
}
|
||||
} else if (entry.isFile() && entry.name.endsWith('.md') && entry.name !== 'README.md') {
|
||||
// Skip non-role files
|
||||
const skipFiles = ['CATALOG.md', 'UPSTREAM.md', 'CONTRIBUTING.md', 'LICENSE',
|
||||
'AGENT-LIST.md', 'EXECUTIVE-BRIEF.md', 'QUICKSTART.md', 'nexus-strategy.md'];
|
||||
if (skipFiles.includes(entry.name)) continue;
|
||||
if (entry.name.startsWith('bug') || entry.name.startsWith('feature') || entry.name.startsWith('new_agent') || entry.name.startsWith('PULL_REQUEST')) continue;
|
||||
|
||||
this._parseRoleFile(fullPath, category);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_parseRoleFile(filePath, category) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const roleId = path.basename(filePath, '.md');
|
||||
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
||||
|
||||
let name = roleId;
|
||||
let description = '';
|
||||
let emoji = '🤖';
|
||||
let color = 'gray';
|
||||
let body = content;
|
||||
|
||||
if (match) {
|
||||
const frontMatter = match[1];
|
||||
body = match[2].trim();
|
||||
const nameMatch = frontMatter.match(/^name:\s*(.+)$/m);
|
||||
if (nameMatch) name = nameMatch[1].trim();
|
||||
const descMatch = frontMatter.match(/^description:\s*(.+)$/m);
|
||||
if (descMatch) description = descMatch[1].trim();
|
||||
const emojiMatch = frontMatter.match(/^emoji:\s*(.+)$/m);
|
||||
if (emojiMatch) emoji = emojiMatch[1].trim();
|
||||
const colorMatch = frontMatter.match(/^color:\s*(.+)$/m);
|
||||
if (colorMatch) color = colorMatch[1].trim();
|
||||
}
|
||||
|
||||
const role = {
|
||||
id: roleId,
|
||||
name,
|
||||
description,
|
||||
emoji,
|
||||
color,
|
||||
category: category || 'uncategorized',
|
||||
body,
|
||||
filePath,
|
||||
fullPrompt: body // The full markdown body serves as the system prompt template
|
||||
};
|
||||
|
||||
this.roles.set(roleId, role);
|
||||
|
||||
// Index by category
|
||||
const cat = category || 'uncategorized';
|
||||
if (!this.byCategory.has(cat)) this.byCategory.set(cat, []);
|
||||
this.byCategory.get(cat).push(roleId);
|
||||
|
||||
// Index keywords from name and description
|
||||
const keywords = [...new Set(
|
||||
(name + ' ' + description + ' ' + roleId)
|
||||
.toLowerCase()
|
||||
.split(/[\s,,、()()\/\\\-_]+/)
|
||||
.filter(k => k.length > 1)
|
||||
)];
|
||||
for (const kw of keywords) {
|
||||
if (!this.byKeyword.has(kw)) this.byKeyword.set(kw, new Set());
|
||||
this.byKeyword.get(kw).add(roleId);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[RoleTemplates] Error parsing ${filePath}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
getTemplate(roleId) {
|
||||
const role = this.roles.get(roleId);
|
||||
if (!role) return null;
|
||||
return {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
emoji: role.emoji,
|
||||
color: role.color,
|
||||
category: role.category,
|
||||
prompt: role.fullPrompt
|
||||
};
|
||||
}
|
||||
|
||||
searchRoles(query) {
|
||||
const q = query.toLowerCase();
|
||||
const results = [];
|
||||
for (const role of this.roles.values()) {
|
||||
if (role.name.toLowerCase().includes(q) ||
|
||||
role.description.toLowerCase().includes(q) ||
|
||||
role.id.toLowerCase().includes(q) ||
|
||||
role.category.toLowerCase().includes(q)) {
|
||||
results.push({
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
emoji: role.emoji,
|
||||
category: role.category,
|
||||
description: role.description
|
||||
});
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
searchByKeywords(keywords) {
|
||||
const matched = new Set();
|
||||
for (const kw of keywords.map(k => k.toLowerCase()).filter(k => k.length > 1)) {
|
||||
const ids = this.byKeyword.get(kw);
|
||||
if (ids) ids.forEach(id => matched.add(id));
|
||||
}
|
||||
return [...matched].map(id => ({
|
||||
id,
|
||||
name: this.roles.get(id).name,
|
||||
emoji: this.roles.get(id).emoji,
|
||||
category: this.roles.get(id).category,
|
||||
description: this.roles.get(id).description
|
||||
}));
|
||||
}
|
||||
|
||||
getCategories() {
|
||||
const result = [];
|
||||
for (const [cat, roleIds] of this.byCategory) {
|
||||
result.push({
|
||||
category: cat,
|
||||
count: roleIds.length,
|
||||
roles: roleIds.map(id => ({
|
||||
id,
|
||||
name: this.roles.get(id).name,
|
||||
emoji: this.roles.get(id).emoji
|
||||
}))
|
||||
});
|
||||
}
|
||||
return result.sort((a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
getCategoryRoles(category) {
|
||||
const ids = this.byCategory.get(category);
|
||||
if (!ids) return [];
|
||||
return ids.map(id => this.getTemplate(id));
|
||||
}
|
||||
|
||||
getRoleCount() {
|
||||
return this.roles.size;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RoleTemplates;
|
||||
255
orchestrator-v2/run-report.js
Normal file
255
orchestrator-v2/run-report.js
Normal file
@@ -0,0 +1,255 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const API_KEY = process.env.LLM_API_KEY || 'sk-101284b1159f41f68bc1e565972d89e8';
|
||||
const BASE_URL = process.env.LLM_BASE_URL || 'https://api.deepseek.com/v1';
|
||||
const MODEL = process.env.LLM_MODEL || 'deepseek-chat';
|
||||
|
||||
// Product context given to every module
|
||||
const PRODUCT_CONTEXT = `
|
||||
产品名称:小白龙(BaiLongma)
|
||||
产品定位:本地私有化部署的AI Agent框架,面向开发者和技术团队
|
||||
核心能力:
|
||||
- 多Agent并行编排(236个角色模板,28个分类)
|
||||
- 5层持久化引擎(SQLite会话+Memory Provider+Background Review+Curator+Context Compressor)
|
||||
- 自进化闭环:从经验创建技能→使用中改进技能→主动持久化知识→跨会话构建深度理解
|
||||
- 48个内置工具+9个已安装工具,可扩展
|
||||
- ACUI可视化卡片推送(WeatherCard等Web Components)
|
||||
- 全平台消息接入(本地TUI/微信/Discord/飞书/企业微信)
|
||||
- 隐私优先:所有数据存储在本地,LLM调用可选(DeepSeek/其他)
|
||||
- 已解除安全沙箱限制
|
||||
|
||||
产品形态:
|
||||
- 底层:AI Agent框架(Node.js)
|
||||
- 编排层:orchestrator-v2 v3.0
|
||||
- 交互层:ACUI Web组件
|
||||
- 部署方式:本地安装(npm/手动)
|
||||
|
||||
目标用户:
|
||||
- 个人开发者(技术极客)
|
||||
- 中小企业技术团队
|
||||
- AI初创公司
|
||||
- 企业内部工具团队
|
||||
|
||||
差异化优势:
|
||||
- 真正本地私有部署(数据不出域)
|
||||
- 开箱即用的多Agent编排
|
||||
- 自进化能力(记忆→技能→提升)
|
||||
- 多平台消息统一接入
|
||||
`;
|
||||
|
||||
const modules = [
|
||||
{
|
||||
id: 'module_1_market',
|
||||
name: '市场调研与竞争格局分析师',
|
||||
prompt: `你是资深AI行业市场分析师。请基于以下产品信息,撰写一份完整的市场调研与竞争格局分析报告。
|
||||
|
||||
${PRODUCT_CONTEXT}
|
||||
|
||||
请覆盖以下内容:
|
||||
1. **AI Agent市场规模**:全球及中国AI Agent市场规模(当前估值+2026-2028年CAGR预测),细分赛道规模
|
||||
2. **竞争格局地图**:列出主要竞品(AutoGPT、Dify、Coze、LangChain、CrewAI等)并按以下维度对比:开源/闭源、本地部署能力、多Agent支持、易用性、生态成熟度、定价模式
|
||||
3. **SWOT分析**:小白龙的优势(Strengths)、劣势(Weaknesses)、机会(Opportunities)、威胁(Threats)
|
||||
4. **市场空白定位**:哪些细分市场目前无人占据,小白龙最有可能切入的蓝海位置
|
||||
5. **目标市场规模(TAM/SAM/SOM)**:估算可寻址市场总量
|
||||
|
||||
用数据说话,给出具体数字和来源引用格式(即使是大致估算也要有逻辑推导过程)。每个部分不少于300字。`
|
||||
},
|
||||
{
|
||||
id: 'module_2_customer',
|
||||
name: '目标客户与定价策略分析师',
|
||||
prompt: `你是资深SaaS商业化顾问。请基于以下产品信息,撰写一份完整的目标客户分析与定价策略报告。
|
||||
|
||||
${PRODUCT_CONTEXT}
|
||||
|
||||
请覆盖以下内容:
|
||||
1. **ICP(理想客户画像)**:定义3-5个细分客户群体的画像(包括公司规模、行业、技术成熟度、痛点、预算范围、决策链)
|
||||
2. **客户需求层次分析**:每个群体的核心需求→期望需求→兴奋需求
|
||||
3. **定价策略建议**:
|
||||
- 三层产品漏斗设计(引流层/付费层/企业层)
|
||||
- 每个层的功能边界和定价建议(具体数字)
|
||||
- 价值锚定策略(对比竞品定价逻辑)
|
||||
4. **客户生命周期价值估算**:CAC、LTV、LTV/CAC ratio,付费转化率假设
|
||||
5. **获客优先级**:按"市场吸引力×可进入性"矩阵排序各客户群
|
||||
|
||||
每个部分不少于300字。给出具体数字和逻辑推导。`
|
||||
},
|
||||
{
|
||||
id: 'module_3_marketing',
|
||||
name: '市场宣传与获客渠道分析师',
|
||||
prompt: `你是AI产品增长黑客。请基于以下产品信息,撰写一份完整的市场宣传与获客渠道策略报告。
|
||||
|
||||
${PRODUCT_CONTEXT}
|
||||
|
||||
请覆盖以下内容:
|
||||
1. **价值主张提炼**:一句话定位(Elevator Pitch)、3个核心卖点、品牌调性建议
|
||||
2. **内容营销策略**:
|
||||
- 博客/技术文章选题策略(SEO关键词规划)
|
||||
- 视频/Demo内容(B站/YouTube)
|
||||
- 开源社区运营(GitHub Star增长策略)
|
||||
- 技术大会/Meetup演讲策略
|
||||
3. **渠道策略**:
|
||||
- 国内渠道:知乎、掘金、B站、公众号、开源中国、CSDN
|
||||
- 海外渠道:Hacker News、Reddit、Dev.to、Twitter/X
|
||||
- 每个渠道的预期效果(曝光量/转化率)
|
||||
4. **冷启动计划**:前90天具体行动计划(每周关键动作)
|
||||
5. **预算分配**:假设月预算2万元,按渠道分配建议
|
||||
6. **关键指标(KPIs)**:定义北极星指标和过程指标
|
||||
|
||||
每个部分不少于250字。`
|
||||
},
|
||||
{
|
||||
id: 'module_4_financial',
|
||||
name: '财务模型分析师',
|
||||
prompt: `你是AI初创公司财务分析师。请基于以下产品信息,撰写一份完整的3年财务模型报告。
|
||||
|
||||
${PRODUCT_CONTEXT}
|
||||
|
||||
请覆盖以下内容:
|
||||
1. **收入模型**:
|
||||
- 三层产品线的收入假设(免费用户数→付费转化率→客单价)
|
||||
- 月/年收入预测(36个月)
|
||||
- 收入结构比例(免费引流占比/付费占比/企业占比)
|
||||
2. **成本结构**:
|
||||
- 开发成本(假设1人全栈开发)
|
||||
- 服务器/基础设施成本
|
||||
- 营销成本
|
||||
- API调用成本(LLM调用费用)
|
||||
- 其他运营成本
|
||||
3. **盈利预测**:
|
||||
- 月度损益表(前36个月)
|
||||
- 盈亏平衡点预测(第几个月)
|
||||
- 毛利率变化趋势
|
||||
4. **关键假设与敏感度分析**:
|
||||
- 最乐观/基准/最悲观三种场景
|
||||
- 哪个变量最敏感(付费转化率/客单价/流失率)
|
||||
5. **融资建议**:
|
||||
- 何时需要融资、融多少
|
||||
- 估值逻辑(对比可比公司)
|
||||
|
||||
所有数字基于合理假设,标注出假设依据。每个部分不少于300字。`
|
||||
},
|
||||
{
|
||||
id: 'module_5_risk',
|
||||
name: '风险分析与进入路径策略师',
|
||||
prompt: `你是AI领域风险顾问。请基于以下产品信息,撰写一份完整的风险分析与进入路径策略报告。
|
||||
|
||||
${PRODUCT_CONTEXT}
|
||||
|
||||
请覆盖以下内容:
|
||||
1. **市场风险**:大厂进入竞争(字节Coze/百度)、开源替代品威胁、市场教育成本
|
||||
2. **技术风险**:LLM依赖风险(API成本/模型更换/隐私)、技术债累积、架构扩展性瓶颈
|
||||
3. **商业风险**:付费意愿低、盈利模式不确定、差异化被抹平
|
||||
4. **运营风险**:个人/小团队瓶颈、支持成本、社区运营负担
|
||||
5. **风险矩阵**:按可能性×影响程度排序,标出每个风险的缓解策略
|
||||
6. **进入路径建议**:
|
||||
- 阶段一(0-3个月):MVP验证期,关键里程碑
|
||||
- 阶段二(3-6个月):早期用户期,关键里程碑
|
||||
- 阶段三(6-12个月):增长期,关键里程碑
|
||||
- 阶段四(12-24个月):规模化期,关键里程碑
|
||||
7. **退出策略**:如果失败,资产如何变现/转型方向
|
||||
|
||||
每个部分不少于250字。`
|
||||
}
|
||||
];
|
||||
|
||||
async function callLLM(messages, temperature = 0.7) {
|
||||
const url = `${BASE_URL}/chat/completions`;
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${API_KEY}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: MODEL,
|
||||
messages,
|
||||
temperature,
|
||||
max_tokens: 4096
|
||||
})
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.text();
|
||||
throw new Error(`LLM API error ${resp.status}: ${err}`);
|
||||
}
|
||||
const data = await resp.json();
|
||||
return data.choices[0].message.content;
|
||||
}
|
||||
|
||||
async function runModule(mod) {
|
||||
console.log(`[${new Date().toLocaleTimeString()}] 开始执行: ${mod.name}`);
|
||||
const start = Date.now();
|
||||
try {
|
||||
const content = await callLLM([
|
||||
{ role: 'system', content: `你是${mod.name}。请输出结构化Markdown报告。` },
|
||||
{ role: 'user', content: mod.prompt }
|
||||
]);
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
console.log(`[${new Date().toLocaleTimeString()}] ${mod.name} 完成 (${elapsed}s, ${content.length} chars)`);
|
||||
return { id: mod.id, name: mod.name, content, ok: true };
|
||||
} catch (err) {
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
console.error(`[${new Date().toLocaleTimeString()}] ${mod.name} 失败 (${elapsed}s): ${err.message}`);
|
||||
return { id: mod.id, name: mod.name, error: err.message, ok: false };
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('=== BaiLongma 商业化全维度分析报告 ===');
|
||||
console.log(`模型: ${MODEL}, 时间: ${new Date().toISOString()}`);
|
||||
console.log(`产品: 小白龙 (BaiLongma) orchestrator-v2 v3.0`);
|
||||
console.log(`模块数: ${modules.length}\n`);
|
||||
|
||||
// Run all 5 modules in parallel
|
||||
const results = await Promise.all(modules.map(m => runModule(m)));
|
||||
|
||||
// Check results
|
||||
const successes = results.filter(r => r.ok);
|
||||
const failures = results.filter(r => !r.ok);
|
||||
console.log(`\n完成: ${successes.length}/${modules.length}, 失败: ${failures.length}`);
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.log('失败模块:', failures.map(f => `[${f.id}] ${f.name}: ${f.error}`).join('\\n'));
|
||||
}
|
||||
|
||||
// Build combined prompt for summarizer
|
||||
const combined = successes.map(r =>
|
||||
`===== ${r.name} =====\n${r.content}`
|
||||
).join('\n\n');
|
||||
|
||||
console.log('\n===== 汇总Agent开始整合报告 =====');
|
||||
const finalReport = await callLLM([
|
||||
{
|
||||
role: 'system',
|
||||
content: '你是顶级商业策略分析师。你要将5个专业领域分析报告整合成一份完整、连贯、可执行的商业化报告。'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `以下是对"小白龙(BaiLongma)"AI Agent框架的5个维度分析结果。请将它们整合成一份完整商业化报告。
|
||||
|
||||
要求:
|
||||
1. **结构**:完整报告结构,加入执行摘要(Executive Summary)放在最前面
|
||||
2. **去重整合**:5份报告中重叠的内容合并,矛盾的观点做权衡判断
|
||||
3. **可执行性**:每部分结尾给出"核心建议"(Action Item)
|
||||
4. **格式**:Markdown格式,加目录,适合直接阅读
|
||||
5. **标题**:用中文,加英文副标题
|
||||
|
||||
以下是5个模块的原始输出:
|
||||
|
||||
${combined}`
|
||||
}
|
||||
], 0.5);
|
||||
|
||||
console.log(`\n汇总报告完成: ${finalReport.length} chars`);
|
||||
|
||||
// Write to file
|
||||
const outputPath = path.join(__dirname, 'report.md');
|
||||
fs.writeFileSync(outputPath, finalReport, 'utf8');
|
||||
console.log(`\n报告已写入: ${outputPath}`);
|
||||
console.log(`文件大小: ${fs.statSync(outputPath).size} bytes`);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Fatal:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
254
orchestrator-v2/run-v2.js
Normal file
254
orchestrator-v2/run-v2.js
Normal file
@@ -0,0 +1,254 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Auto-load .env
|
||||
var envPath = path.join(__dirname, '.env');
|
||||
if (fs.existsSync(envPath)) {
|
||||
var lines = fs.readFileSync(envPath, 'utf8').split('\n');
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var l = lines[i].trim();
|
||||
if (!l || l.startsWith('#')) continue;
|
||||
var eq = l.indexOf('=');
|
||||
if (eq < 0) continue;
|
||||
var k = l.slice(0, eq).trim();
|
||||
var v = l.slice(eq + 1).trim();
|
||||
if (!process.env[k]) process.env[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
var Coordinator = require('./coordinator.js');
|
||||
var RoleRouter = require('./role-router.js');
|
||||
|
||||
async function main() {
|
||||
var args = process.argv.slice(2);
|
||||
|
||||
// --stats: Show system stats from all layers
|
||||
if (args.includes('--stats') || args.includes('-s')) {
|
||||
var coord = new Coordinator();
|
||||
var stats = await coord.getStats();
|
||||
console.log('=== BaiLongma Orchestrator v3.0 — System Stats ===\n');
|
||||
console.log('-- Sessions --');
|
||||
console.log(' Total:', stats.sessions.total_sessions || 0);
|
||||
console.log(' Completed:', stats.sessions.completed || 0);
|
||||
console.log(' Failed:', stats.sessions.failed || 0);
|
||||
console.log(' Total Tokens:', stats.sessions.total_tokens || 0);
|
||||
console.log(' Compressed:', stats.sessions.compressed || 0);
|
||||
console.log('');
|
||||
console.log('-- Memories --');
|
||||
console.log(' Total:', stats.memories.total || 0);
|
||||
console.log(' Types:', JSON.stringify(stats.memories.byType || {}));
|
||||
console.log(' File:', stats.memories.file || 'N/A');
|
||||
console.log('');
|
||||
console.log('-- Reviews --');
|
||||
console.log(' Total reviews:', stats.reviews.total || 0);
|
||||
console.log(' Style signals:', stats.reviews.styleSignals || 0);
|
||||
console.log(' Skill signals:', stats.reviews.skillSignals || 0);
|
||||
await coord.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// --curator: Run curator for skill/memory maintenance
|
||||
if (args.includes('--curator') || args.includes('-c')) {
|
||||
var coord = new Coordinator();
|
||||
var report = await coord.runCurator();
|
||||
console.log('\n=== Curator Report ===');
|
||||
console.log('Timestamp:', report.timestamp);
|
||||
console.log('Stale skills (' + report.staleSkills.length + '):');
|
||||
for (var i = 0; i < report.staleSkills.length; i++) {
|
||||
console.log(' - ' + report.staleSkills[i].name + ' (' + report.staleSkills[i].ageDays + ' days old)');
|
||||
}
|
||||
if (report.memoryAnalysis) {
|
||||
console.log('\nMemory Analysis:');
|
||||
console.log(' Total:', report.memoryAnalysis.totalMemories);
|
||||
console.log(' Suggestions:', report.memoryAnalysis.suggestions.join(', ') || 'none');
|
||||
}
|
||||
await coord.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// --search <query>: Cross-layer search
|
||||
var searchIdx = args.indexOf('--search');
|
||||
if (searchIdx >= 0 && searchIdx + 1 < args.length) {
|
||||
var query = args[searchIdx + 1];
|
||||
var coord = new Coordinator();
|
||||
var results = await coord.search(query, 10);
|
||||
console.log('=== Search Results for "' + query + '" ===\n');
|
||||
console.log('-- Sessions (' + results.sessions.length + ') --');
|
||||
for (var i = 0; i < results.sessions.length; i++) {
|
||||
var s = results.sessions[i];
|
||||
console.log(' [' + s.status + '] ' + s.id + ': ' + (s.task || '').slice(0, 80));
|
||||
}
|
||||
console.log('\n-- Messages (' + results.messages.length + ') --');
|
||||
for (var i = 0; i < results.messages.length; i++) {
|
||||
var m = results.messages[i];
|
||||
console.log(' [' + m.role + '] ' + (m.content || '').slice(0, 100));
|
||||
}
|
||||
if (results.memories && results.memories.length) {
|
||||
console.log('\n-- Memories (' + results.memories.length + ') --');
|
||||
for (var i = 0; i < results.memories.length; i++) {
|
||||
var mem = results.memories[i];
|
||||
console.log(' [' + mem.type + '] ' + (mem.title || '') + ': ' + (mem.content || '').slice(0, 80));
|
||||
}
|
||||
}
|
||||
await coord.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// --sessions: List recent sessions
|
||||
if (args.includes('--sessions') || args.includes('-l')) {
|
||||
var coord = new Coordinator();
|
||||
await coord.init();
|
||||
var sessions = coord.listSessions(20);
|
||||
console.log('=== Recent Sessions (' + sessions.length + ') ===\n');
|
||||
for (var i = 0; i < sessions.length; i++) {
|
||||
var s = sessions[i];
|
||||
console.log(' [' + s.status + '] ' + s.id);
|
||||
console.log(' Task: ' + (s.task || '').slice(0, 60));
|
||||
console.log(' Tokens: ' + (s.token_count || 0) + ' | Compressed: ' + (s.is_compressed ? 'yes' : 'no'));
|
||||
console.log(' Created: ' + s.created_at);
|
||||
console.log('');
|
||||
}
|
||||
await coord.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// --chain <sessionId>: Show session compression chain
|
||||
var chainIdx = args.indexOf('--chain');
|
||||
if (chainIdx >= 0 && chainIdx + 1 < args.length) {
|
||||
var sessionId = args[chainIdx + 1];
|
||||
var coord = new Coordinator();
|
||||
await coord.init();
|
||||
var chain = coord.getSessionChain(sessionId);
|
||||
console.log('=== Session Chain for ' + sessionId + ' (' + chain.length + ' hops) ===\n');
|
||||
for (var i = 0; i < chain.length; i++) {
|
||||
var s = chain[i];
|
||||
console.log(' [' + (i + 1) + '] ' + s.id + ' [' + s.status + ']' + (s.is_compressed ? ' [COMPRESSED]' : ''));
|
||||
console.log(' Task: ' + (s.task || '').slice(0, 60));
|
||||
if (s.summary) console.log(' Summary: ' + s.summary.slice(0, 100));
|
||||
console.log('');
|
||||
}
|
||||
await coord.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// --list-roles: List all role templates
|
||||
if (args.includes('--list-roles')) {
|
||||
var router = new RoleRouter();
|
||||
await router.init();
|
||||
var cats = router.getTemplates().getCategories();
|
||||
console.log('=== BaiLongma Role Templates (' + router.getTemplates().getRoleCount() + ' roles) ===\n');
|
||||
for (var i = 0; i < cats.length; i++) {
|
||||
console.log(cats[i].category + ' (' + cats[i].count + '):');
|
||||
for (var j = 0; j < cats[i].roles.length; j++) {
|
||||
console.log(' ' + cats[i].roles[j].emoji + ' ' + cats[i].roles[j].name);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// --search-roles <query>: Search roles
|
||||
var sri = args.indexOf('--search-roles');
|
||||
if (sri >= 0 && sri + 1 < args.length) {
|
||||
var query = args[sri + 1];
|
||||
var router = new RoleRouter();
|
||||
await router.init();
|
||||
var results = router.getTemplates().searchRoles(query);
|
||||
console.log('=== Search results for "' + query + '" (' + results.length + ' matches) ===\n');
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
console.log(results[i].emoji + ' ' + results[i].name);
|
||||
console.log(' ID: ' + results[i].id + ' | Category: ' + results[i].category);
|
||||
console.log(' ' + results[i].description.slice(0, 100));
|
||||
console.log('');
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// --search-msgs <query>: Search messages (for backward compatibility)
|
||||
var smi = args.indexOf('--search-msgs');
|
||||
if (smi >= 0 && smi + 1 < args.length) {
|
||||
var query = args[smi + 1];
|
||||
var coord = new Coordinator();
|
||||
var results = await coord.search(query);
|
||||
console.log('=== Message search results for "' + query + '" (' + results.messages.length + ') ===\n');
|
||||
for (var i = 0; i < results.messages.length; i++) {
|
||||
var m = results.messages[i];
|
||||
console.log(' [' + m.role + '] ' + (m.content || '').slice(0, 120));
|
||||
console.log(' Session: ' + m.session_id + '\n');
|
||||
}
|
||||
await coord.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
|
||||
// --debate <question>: Run 8-step structured debate
|
||||
var debateIdx = args.indexOf('--debate');
|
||||
if (debateIdx >= 0) {
|
||||
var question = args.slice(debateIdx + 1).join(' ');
|
||||
if (!question) { console.error('Usage: node run-v2.js --debate <your question>'); process.exit(1); }
|
||||
|
||||
console.log('=== BaiLongma 8-Step Structured Debate ===');
|
||||
console.log('Question:', question);
|
||||
console.log('');
|
||||
|
||||
const { runDebate } = require('./debate/coordinator.js');
|
||||
const { saveReport } = require('./debate/report.js');
|
||||
const { DEFAULT_PERSONAS } = require('./debate/personas.js');
|
||||
|
||||
var result = await runDebate(question, {
|
||||
personas: DEFAULT_PERSONAS,
|
||||
model: process.env.MODEL || 'deepseek-chat',
|
||||
maxTokens: 2048
|
||||
});
|
||||
|
||||
var reportPath = path.join(__dirname, 'debate-report.md');
|
||||
saveReport(question, result.state, result.stepLog, reportPath);
|
||||
|
||||
console.log('');
|
||||
console.log('=== Debate Complete ===');
|
||||
console.log('Report saved:', reportPath);
|
||||
console.log('');
|
||||
console.log('--- Summary ---');
|
||||
console.log(result.state.summary ? result.state.summary.slice(0, 500) : '(no summary)');
|
||||
console.log('');
|
||||
console.log('--- Action Items ---');
|
||||
console.log(result.state.harvest ? result.state.harvest.slice(0, 500) : '(no action items)');
|
||||
console.log('');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Normal task execution
|
||||
var task = args.join(' ') || 'Run default analysis task';
|
||||
|
||||
console.log('=== BaiLongma Orchestrator v3.0 (5-Layer Persistence) ===');
|
||||
console.log('Task:', task);
|
||||
console.log('');
|
||||
|
||||
var coord = new Coordinator();
|
||||
var result = await coord.run(task, { source: 'cli', timestamp: new Date().toISOString() });
|
||||
|
||||
console.log('');
|
||||
console.log('=== Results ===');
|
||||
for (var i = 0; i < result.aggregated.summaries.length; i++) {
|
||||
var s = result.aggregated.summaries[i];
|
||||
console.log(' ' + s.summary);
|
||||
if (s.output) {
|
||||
console.log(s.output);
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
console.log('Session ID:', result.sessionId);
|
||||
console.log('Sub-tasks:', result.subTasks.length);
|
||||
console.log('Completed:', result.aggregated.completed);
|
||||
if (result.aggregated.failed > 0) {
|
||||
console.log('Failed:', result.aggregated.failed);
|
||||
}
|
||||
|
||||
await coord.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch(function(err) {
|
||||
console.error('Fatal:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
205
orchestrator-v2/session-store.js
Normal file
205
orchestrator-v2/session-store.js
Normal file
@@ -0,0 +1,205 @@
|
||||
const initSqlJs = require('sql.js');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
class SessionStore {
|
||||
constructor(dbDir) {
|
||||
this.dbDir = dbDir;
|
||||
this.dbPath = path.join(dbDir, 'sessions.sqlite');
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!fs.existsSync(this.dbDir)) fs.mkdirSync(this.dbDir, { recursive: true });
|
||||
const SQL = await initSqlJs();
|
||||
if (fs.existsSync(this.dbPath)) {
|
||||
const buf = fs.readFileSync(this.dbPath);
|
||||
this.db = new SQL.Database(buf);
|
||||
} else {
|
||||
this.db = new SQL.Database();
|
||||
}
|
||||
this.db.run('PRAGMA journal_mode=WAL');
|
||||
this.db.run('PRAGMA foreign_keys=ON');
|
||||
this.db.run("CREATE TABLE IF NOT EXISTS sessions (" +
|
||||
"id TEXT PRIMARY KEY, parent_id TEXT, status TEXT DEFAULT 'created', " +
|
||||
"task TEXT, context TEXT, result TEXT, summary TEXT, " +
|
||||
"token_count INTEGER DEFAULT 0, is_compressed INTEGER DEFAULT 0, " +
|
||||
"events TEXT DEFAULT '[]', " +
|
||||
"created_at TEXT DEFAULT (datetime('now')), " +
|
||||
"updated_at TEXT DEFAULT (datetime('now')), " +
|
||||
"FOREIGN KEY (parent_id) REFERENCES sessions(id))");
|
||||
this.db.run("CREATE TABLE IF NOT EXISTS messages (" +
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, " +
|
||||
"role TEXT NOT NULL, content TEXT, tool_calls TEXT, tool_call_id TEXT, " +
|
||||
"token_count INTEGER DEFAULT 0, " +
|
||||
"created_at TEXT DEFAULT (datetime('now')), " +
|
||||
"FOREIGN KEY (session_id) REFERENCES sessions(id))");
|
||||
this._save();
|
||||
}
|
||||
|
||||
_save() {
|
||||
const data = this.db.export();
|
||||
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
||||
fs.writeFileSync(this.dbPath, buf);
|
||||
}
|
||||
|
||||
createSession(id, task, context, parentId) {
|
||||
if (parentId === undefined) parentId = null;
|
||||
this.db.run('INSERT OR REPLACE INTO sessions (id, parent_id, task, context, status) VALUES (?, ?, ?, ?, ?)',
|
||||
[id, parentId, task, JSON.stringify(context || {}), 'created']);
|
||||
this._save();
|
||||
}
|
||||
|
||||
getSession(id) {
|
||||
const stmt = this.db.exec('SELECT rowid, * FROM sessions WHERE id = ?', [id]);
|
||||
if (!stmt.length || !stmt[0].values.length) return null;
|
||||
const cols = stmt[0].columns;
|
||||
const vals = stmt[0].values[0];
|
||||
return cols.reduce(function(o, c, i) { o[c] = vals[i]; return o; }, {});
|
||||
}
|
||||
|
||||
updateSession(id, updates) {
|
||||
var allowed = ['status','task','context','result','summary','token_count','is_compressed','parent_id'];
|
||||
var sets = [];
|
||||
var params = [];
|
||||
for (var key in updates) {
|
||||
if (allowed.indexOf(key) >= 0) {
|
||||
sets.push(key + ' = ?');
|
||||
params.push(typeof updates[key] === 'object' ? JSON.stringify(updates[key]) : updates[key]);
|
||||
}
|
||||
}
|
||||
if (!sets.length) return;
|
||||
sets.push("updated_at = datetime('now')");
|
||||
params.push(id);
|
||||
this.db.run('UPDATE sessions SET ' + sets.join(', ') + ' WHERE id = ?', params);
|
||||
this._save();
|
||||
}
|
||||
|
||||
updateStatus(id, status, result) {
|
||||
if (result) {
|
||||
this.db.run("UPDATE sessions SET status = ?, result = ?, updated_at = datetime('now') WHERE id = ?",
|
||||
[status, JSON.stringify(result), id]);
|
||||
} else {
|
||||
this.db.run("UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE id = ?",
|
||||
[status, id]);
|
||||
}
|
||||
this._save();
|
||||
}
|
||||
|
||||
deleteSession(id) {
|
||||
this.db.run('DELETE FROM messages WHERE session_id = ?', [id]);
|
||||
this.db.run('DELETE FROM sessions WHERE id = ?', [id]);
|
||||
this._save();
|
||||
}
|
||||
|
||||
listSessions(limit, offset) {
|
||||
if (limit === undefined) limit = 20;
|
||||
if (offset === undefined) offset = 0;
|
||||
var stmt = this.db.exec('SELECT id, parent_id, status, task, summary, token_count, is_compressed, created_at, updated_at FROM sessions ORDER BY created_at DESC LIMIT ' + limit + ' OFFSET ' + offset);
|
||||
if (!stmt.length) return [];
|
||||
var cols = stmt[0].columns;
|
||||
return stmt[0].values.map(function(v) { return cols.reduce(function(o, c, i) { o[c] = v[i]; return o; }, {}); });
|
||||
}
|
||||
|
||||
getSessionChain(id) {
|
||||
var chain = [];
|
||||
var current = this.getSession(id);
|
||||
while (current) {
|
||||
chain.unshift(current);
|
||||
if (current.parent_id) current = this.getSession(current.parent_id);
|
||||
else break;
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
appendEvent(id, event) {
|
||||
var row = this.db.exec('SELECT events FROM sessions WHERE id = ?', [id]);
|
||||
if (row.length > 0) {
|
||||
var events = JSON.parse(row[0].values[0][0] || '[]');
|
||||
events.push(event);
|
||||
this.db.run("UPDATE sessions SET events = ?, updated_at = datetime('now') WHERE id = ?", [JSON.stringify(events), id]);
|
||||
this._save();
|
||||
}
|
||||
}
|
||||
|
||||
addMessage(sessionId, role, content, extras) {
|
||||
if (!extras) extras = {};
|
||||
this.db.run('INSERT INTO messages (session_id, role, content, tool_calls, tool_call_id, token_count) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[sessionId, role, content, extras.toolCalls ? JSON.stringify(extras.toolCalls) : null, extras.toolCallId || null, extras.tokenCount || 0]);
|
||||
this._save();
|
||||
if (extras.tokenCount) {
|
||||
this.db.run("UPDATE sessions SET token_count = token_count + ?, updated_at = datetime('now') WHERE id = ?", [extras.tokenCount, sessionId]);
|
||||
this._save();
|
||||
}
|
||||
}
|
||||
|
||||
getMessages(sessionId, limit) {
|
||||
if (limit === undefined) limit = 100;
|
||||
var stmt = this.db.exec('SELECT id, role, content, tool_calls, token_count, created_at FROM messages WHERE session_id = ? ORDER BY id ASC LIMIT ?', [sessionId, limit]);
|
||||
if (!stmt.length) return [];
|
||||
var cols = stmt[0].columns;
|
||||
return stmt[0].values.map(function(v) { return cols.reduce(function(o, c, i) { o[c] = v[i]; return o; }, {}); });
|
||||
}
|
||||
|
||||
countMessages(sessionId) {
|
||||
var stmt = this.db.exec('SELECT COUNT(*) as cnt FROM messages WHERE session_id = ?', [sessionId]);
|
||||
return stmt.length ? stmt[0].values[0][0] : 0;
|
||||
}
|
||||
|
||||
getTotalTokenCount(sessionId) {
|
||||
var stmt = this.db.exec('SELECT SUM(token_count) as total FROM messages WHERE session_id = ?', [sessionId]);
|
||||
return (stmt.length && stmt[0].values[0][0]) ? stmt[0].values[0][0] : 0;
|
||||
}
|
||||
|
||||
searchSessions(query, limit) {
|
||||
if (limit === undefined) limit = 10;
|
||||
var likeQ = '%' + query + '%';
|
||||
var stmt = this.db.exec('SELECT id, task, summary, status, created_at FROM sessions WHERE task LIKE ? OR summary LIKE ? ORDER BY created_at DESC LIMIT ?', [likeQ, likeQ, limit]);
|
||||
if (!stmt.length) return [];
|
||||
var cols = stmt[0].columns;
|
||||
return stmt[0].values.map(function(v) { return cols.reduce(function(o, c, i) { o[c] = v[i]; return o; }, {}); });
|
||||
}
|
||||
|
||||
searchMessages(query, limit) {
|
||||
if (limit === undefined) limit = 20;
|
||||
var likeQ = '%' + query + '%';
|
||||
var stmt = this.db.exec('SELECT session_id, role, content FROM messages WHERE content LIKE ? ORDER BY id DESC LIMIT ?', [likeQ, limit]);
|
||||
if (!stmt.length) return [];
|
||||
var cols = stmt[0].columns;
|
||||
return stmt[0].values.map(function(v) { return cols.reduce(function(o, c, i) { o[c] = v[i]; return o; }, {}); });
|
||||
}
|
||||
|
||||
compressSession(id, summary) {
|
||||
this.db.run("UPDATE sessions SET is_compressed = 1, summary = ?, status = 'compressed', updated_at = datetime('now') WHERE id = ?", [summary, id]);
|
||||
this._save();
|
||||
}
|
||||
|
||||
getCompressibleSessions(threshold) {
|
||||
if (threshold === undefined) threshold = 100;
|
||||
var stmt = this.db.exec("SELECT s.id, s.task, COUNT(m.id) as msg_count, s.created_at FROM sessions s LEFT JOIN messages m ON m.session_id = s.id WHERE s.is_compressed = 0 AND s.status = 'completed' GROUP BY s.id HAVING msg_count > ? ORDER BY msg_count DESC", [threshold]);
|
||||
if (!stmt.length) return [];
|
||||
var cols = stmt[0].columns;
|
||||
return stmt[0].values.map(function(v) { return cols.reduce(function(o, c, i) { o[c] = v[i]; return o; }, {}); });
|
||||
}
|
||||
|
||||
getStats() {
|
||||
var stmt = this.db.exec("SELECT COUNT(*) as total_sessions, SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END) as completed, SUM(CASE WHEN status='failed' THEN 1 ELSE 0 END) as failed, SUM(token_count) as total_tokens, SUM(CASE WHEN is_compressed=1 THEN 1 ELSE 0 END) as compressed FROM sessions");
|
||||
if (!stmt.length) return {};
|
||||
var vals = stmt[0].values[0];
|
||||
var cols = stmt[0].columns;
|
||||
return cols.reduce(function(o, c, i) { o[c] = vals[i]; return o; }, {});
|
||||
}
|
||||
|
||||
searchEverything(query, limit) {
|
||||
if (limit === undefined) limit = 10;
|
||||
var results = { sessions: [], messages: [] };
|
||||
try { results.sessions = this.searchSessions(query, limit); results.messages = this.searchMessages(query, limit * 2); } catch (e) {}
|
||||
return results;
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.db) { this._save(); this.db.close(); }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SessionStore;
|
||||
Reference in New Issue
Block a user