@lightcone-ai/daemon 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +21 -0
- package/src/agent-manager.js +213 -0
- package/src/chat-bridge.js +241 -0
- package/src/connection.js +71 -0
- package/src/drivers/claude.js +320 -0
- package/src/index.js +43 -0
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lightcone-ai/daemon",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lightcone-daemon": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "node src/index.js",
|
|
14
|
+
"dev": "node --watch src/index.js"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
18
|
+
"dotenv": "^17.4.0",
|
|
19
|
+
"ws": "^8.20.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { mkdirSync } from 'fs';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { buildSystemPrompt } from './drivers/claude.js';
|
|
6
|
+
|
|
7
|
+
export class AgentManager {
|
|
8
|
+
constructor({ serverUrl, machineApiKey }) {
|
|
9
|
+
this.serverUrl = serverUrl;
|
|
10
|
+
this.machineApiKey = machineApiKey;
|
|
11
|
+
// agentId → { config, sessionId, proc }
|
|
12
|
+
this.agents = new Map();
|
|
13
|
+
// agentId → true (spawn in progress, prevent double-spawn)
|
|
14
|
+
this.starting = new Set();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
handle(msg, connection) {
|
|
18
|
+
switch (msg.type) {
|
|
19
|
+
case 'agent:start': return this._startAgent(msg, connection);
|
|
20
|
+
case 'agent:stop': return this._stopAgent(msg.agentId, connection);
|
|
21
|
+
case 'agent:deliver': return this._deliverMessage(msg, connection);
|
|
22
|
+
case 'ping': return connection.send({ type: 'pong' });
|
|
23
|
+
default:
|
|
24
|
+
console.log(`[AgentManager] Unhandled: ${msg.type}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
stopAll() {
|
|
29
|
+
for (const [agentId, agent] of this.agents) {
|
|
30
|
+
if (agent.proc) agent.proc.kill();
|
|
31
|
+
}
|
|
32
|
+
this.agents.clear();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── private ───────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
_workspaceDir(agentId) {
|
|
38
|
+
const dir = path.join(homedir(), '.lightcone', 'agents', agentId);
|
|
39
|
+
mkdirSync(dir, { recursive: true });
|
|
40
|
+
return dir;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_startAgent({ agentId, config }, connection) {
|
|
44
|
+
if (this.agents.has(agentId) || this.starting.has(agentId)) {
|
|
45
|
+
console.log(`[AgentManager] Agent ${agentId} already registered`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.starting.add(agentId);
|
|
49
|
+
|
|
50
|
+
const workspaceDir = this._workspaceDir(agentId);
|
|
51
|
+
const chatBridgePath = new URL('./chat-bridge.js', import.meta.url).pathname;
|
|
52
|
+
|
|
53
|
+
const mcpServers = {
|
|
54
|
+
chat: {
|
|
55
|
+
command: 'node',
|
|
56
|
+
args: [chatBridgePath],
|
|
57
|
+
env: {
|
|
58
|
+
SERVER_URL: this.serverUrl,
|
|
59
|
+
MACHINE_API_KEY: this.machineApiKey,
|
|
60
|
+
AGENT_ID: agentId,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (config.browserAccess) {
|
|
66
|
+
mcpServers['chrome-devtools'] = {
|
|
67
|
+
command: 'npx',
|
|
68
|
+
args: ['chrome-devtools-mcp@latest', '--headless'],
|
|
69
|
+
env: {},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (config.mysqlAccess) {
|
|
74
|
+
const mysqlServerPath = new URL('../../mcp-servers/mysql/index.js', import.meta.url).pathname;
|
|
75
|
+
mcpServers['mysql'] = {
|
|
76
|
+
command: 'node',
|
|
77
|
+
args: [mysqlServerPath],
|
|
78
|
+
env: {
|
|
79
|
+
DB_HOST: process.env.DB_HOST ?? '',
|
|
80
|
+
DB_PORT: process.env.DB_PORT ?? '3306',
|
|
81
|
+
DB_USER: process.env.DB_USER ?? '',
|
|
82
|
+
DB_PASSWORD: process.env.DB_PASSWORD ?? '',
|
|
83
|
+
DB_NAME: process.env.DB_NAME ?? '',
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const mcpConfig = { mcpServers };
|
|
89
|
+
|
|
90
|
+
const args = [
|
|
91
|
+
'--allow-dangerously-skip-permissions',
|
|
92
|
+
'--dangerously-skip-permissions',
|
|
93
|
+
'--verbose',
|
|
94
|
+
'--output-format', 'stream-json',
|
|
95
|
+
'--input-format', 'stream-json',
|
|
96
|
+
'--mcp-config', JSON.stringify(mcpConfig),
|
|
97
|
+
'--system-prompt', buildSystemPrompt(config, agentId),
|
|
98
|
+
'--disallowed-tools', 'EnterPlanMode,ExitPlanMode',
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
if (config.sessionId) {
|
|
102
|
+
args.push('--resume', config.sessionId);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const spawnEnv = { ...process.env, FORCE_COLOR: '0', ...(config.envVars ?? {}) };
|
|
106
|
+
delete spawnEnv.CLAUDECODE;
|
|
107
|
+
|
|
108
|
+
console.log(`[AgentManager] Spawning claude for ${agentId} (session=${config.sessionId ?? 'new'})`);
|
|
109
|
+
|
|
110
|
+
const proc = spawn('claude', args, {
|
|
111
|
+
cwd: workspaceDir,
|
|
112
|
+
env: spawnEnv,
|
|
113
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
this.agents.set(agentId, { config, sessionId: config.sessionId ?? null, proc });
|
|
117
|
+
this.starting.delete(agentId);
|
|
118
|
+
|
|
119
|
+
// Parse stdout stream for session ID and activity updates
|
|
120
|
+
let buffer = '';
|
|
121
|
+
proc.stdout.on('data', (chunk) => {
|
|
122
|
+
buffer += chunk.toString();
|
|
123
|
+
const lines = buffer.split('\n');
|
|
124
|
+
buffer = lines.pop();
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
if (!line.trim()) continue;
|
|
127
|
+
this._parseLine(agentId, line, connection);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
proc.stderr.on('data', (data) => {
|
|
132
|
+
const text = data.toString().trim();
|
|
133
|
+
if (text) console.error(`[AgentManager][${agentId}] stderr:`, text.slice(0, 300));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
proc.on('exit', (code) => {
|
|
137
|
+
console.log(`[AgentManager] Agent ${agentId} exited (code=${code})`);
|
|
138
|
+
this.agents.delete(agentId);
|
|
139
|
+
connection.send({ type: 'agent:status', agentId, status: 'inactive' });
|
|
140
|
+
connection.send({ type: 'agent:activity', agentId, activity: 'offline', detail: '', entries: [] });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Send startup prompt — agent reads memory index first, then checks messages
|
|
144
|
+
this._write(agentId, 'You have just started. Follow your startup sequence: first call read_memory with path="MEMORY.md" to load your memory index, then call check_messages.');
|
|
145
|
+
|
|
146
|
+
connection.send({ type: 'agent:status', agentId, status: 'active' });
|
|
147
|
+
connection.send({ type: 'agent:activity', agentId, activity: 'online', detail: '', entries: [] });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
_stopAgent(agentId, connection) {
|
|
151
|
+
const agent = this.agents.get(agentId);
|
|
152
|
+
if (!agent) return;
|
|
153
|
+
console.log(`[AgentManager] Stopping agent ${agentId}`);
|
|
154
|
+
agent.proc?.kill();
|
|
155
|
+
// exit handler will report status
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
_deliverMessage({ agentId, seq, message }, connection) {
|
|
159
|
+
connection.send({ type: 'agent:deliver:ack', agentId, seq });
|
|
160
|
+
|
|
161
|
+
if (!this.agents.has(agentId)) {
|
|
162
|
+
console.warn(`[AgentManager] Agent ${agentId} not running, dropping seq=${seq}`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const text = `New message in ${message.channel_type === 'dm' ? `dm from` : `#${message.channel_name} from`} ${message.sender_name}: ${message.content}`;
|
|
167
|
+
console.log(`[AgentManager] Delivering seq=${seq} to agent ${agentId}`);
|
|
168
|
+
this._write(agentId, text);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Write a user message to the running Claude process via stdin
|
|
172
|
+
_write(agentId, text) {
|
|
173
|
+
const agent = this.agents.get(agentId);
|
|
174
|
+
if (!agent?.proc) return;
|
|
175
|
+
const line = JSON.stringify({
|
|
176
|
+
type: 'user',
|
|
177
|
+
message: { role: 'user', content: [{ type: 'text', text }] },
|
|
178
|
+
}) + '\n';
|
|
179
|
+
try {
|
|
180
|
+
agent.proc.stdin.write(line);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error(`[AgentManager] stdin write error for ${agentId}:`, err.message);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
_parseLine(agentId, line, connection) {
|
|
187
|
+
let event;
|
|
188
|
+
try { event = JSON.parse(line); }
|
|
189
|
+
catch { return; }
|
|
190
|
+
|
|
191
|
+
// Capture and sync session ID
|
|
192
|
+
if (event.session_id) {
|
|
193
|
+
const agent = this.agents.get(agentId);
|
|
194
|
+
if (agent && agent.sessionId !== event.session_id) {
|
|
195
|
+
agent.sessionId = event.session_id;
|
|
196
|
+
connection.send({ type: 'agent:session', agentId, sessionId: event.session_id });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Activity state machine
|
|
201
|
+
if (event.type === 'assistant') {
|
|
202
|
+
const hasToolUse = event.message?.content?.some?.(c => c.type === 'tool_use');
|
|
203
|
+
if (hasToolUse) {
|
|
204
|
+
const toolName = event.message.content.find(c => c.type === 'tool_use')?.name ?? 'tool';
|
|
205
|
+
connection.send({ type: 'agent:activity', agentId, activity: 'working', detail: toolName, entries: [] });
|
|
206
|
+
} else {
|
|
207
|
+
connection.send({ type: 'agent:activity', agentId, activity: 'thinking', detail: '', entries: [] });
|
|
208
|
+
}
|
|
209
|
+
} else if (event.type === 'result') {
|
|
210
|
+
connection.send({ type: 'agent:activity', agentId, activity: 'online', detail: '', entries: [] });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
const SERVER_URL = process.env.SERVER_URL ?? 'http://localhost:8777';
|
|
7
|
+
const MACHINE_API_KEY = process.env.MACHINE_API_KEY ?? '';
|
|
8
|
+
const AGENT_ID = process.env.AGENT_ID ?? '';
|
|
9
|
+
|
|
10
|
+
async function api(method, path, body) {
|
|
11
|
+
const url = `${SERVER_URL}/internal/agent/${AGENT_ID}${path}`;
|
|
12
|
+
const res = await fetch(url, {
|
|
13
|
+
method,
|
|
14
|
+
headers: {
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
'Authorization': `Bearer ${MACHINE_API_KEY}`,
|
|
17
|
+
},
|
|
18
|
+
body: body != null ? JSON.stringify(body) : undefined,
|
|
19
|
+
});
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
const text = await res.text();
|
|
22
|
+
throw new Error(`API ${method} ${path} → ${res.status}: ${text}`);
|
|
23
|
+
}
|
|
24
|
+
return res.json();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const server = new McpServer({ name: 'chat', version: '0.1.0' });
|
|
28
|
+
|
|
29
|
+
// ── check_messages ────────────────────────────────────────────────────────────
|
|
30
|
+
server.tool('check_messages', 'Check for new messages in your inbox', {}, async () => {
|
|
31
|
+
const data = await api('GET', '/receive');
|
|
32
|
+
const msgs = data.messages ?? [];
|
|
33
|
+
if (msgs.length === 0) return { content: [{ type: 'text', text: 'No new messages.' }] };
|
|
34
|
+
|
|
35
|
+
const text = msgs.map(m =>
|
|
36
|
+
`[${m.channel_type === 'dm' ? `dm:@${m.channel_name}` : `#${m.channel_name}`}] ${m.sender_name}: ${m.content}`
|
|
37
|
+
+ (m.task_status ? ` [task #${m.task_number} ${m.task_status}]` : '')
|
|
38
|
+
).join('\n');
|
|
39
|
+
return { content: [{ type: 'text', text }] };
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ── send_message ──────────────────────────────────────────────────────────────
|
|
43
|
+
server.tool('send_message', 'Send a message to a channel, DM, or thread', {
|
|
44
|
+
target: z.string().describe('Target: #channel-name | dm:@agentName | #channel-name:shortMsgId'),
|
|
45
|
+
content: z.string().describe('Message content'),
|
|
46
|
+
}, async ({ target, content }) => {
|
|
47
|
+
const data = await api('POST', '/send', { target, content });
|
|
48
|
+
return { content: [{ type: 'text', text: `Sent. messageId=${data.messageId} threadTarget=${data.threadTarget}` }] };
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ── read_history ──────────────────────────────────────────────────────────────
|
|
52
|
+
server.tool('read_history', 'Read message history from a channel', {
|
|
53
|
+
channel: z.string().describe('Target: #channel-name | dm:@agentName'),
|
|
54
|
+
limit: z.number().optional().describe('Max messages to return (default 50)'),
|
|
55
|
+
before: z.number().optional().describe('Return messages before this seq'),
|
|
56
|
+
after: z.number().optional().describe('Return messages after this seq'),
|
|
57
|
+
}, async ({ channel, limit, before, after }) => {
|
|
58
|
+
const params = new URLSearchParams({ channel, limit: String(limit ?? 50) });
|
|
59
|
+
if (before != null) params.set('before', String(before));
|
|
60
|
+
if (after != null) params.set('after', String(after));
|
|
61
|
+
const data = await api('GET', `/history?${params}`);
|
|
62
|
+
const msgs = data.messages ?? [];
|
|
63
|
+
if (msgs.length === 0) return { content: [{ type: 'text', text: 'No messages found.' }] };
|
|
64
|
+
|
|
65
|
+
const text = msgs.map(m =>
|
|
66
|
+
`[seq=${m.seq}] ${m.senderName}: ${m.content}`
|
|
67
|
+
+ (m.taskStatus ? ` [task #${m.taskNumber} ${m.taskStatus}]` : '')
|
|
68
|
+
).join('\n');
|
|
69
|
+
return { content: [{ type: 'text', text }] };
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ── list_server ───────────────────────────────────────────────────────────────
|
|
73
|
+
server.tool('list_server', 'List channels, agents, and humans on the server', {}, async () => {
|
|
74
|
+
const data = await api('GET', '/server');
|
|
75
|
+
const channels = (data.channels ?? []).map(c => ` #${c.name}${c.joined ? ' (joined)' : ''} — ${c.description}`).join('\n');
|
|
76
|
+
const agents = (data.agents ?? []).map(a => ` @${a.name} [${a.status}]`).join('\n');
|
|
77
|
+
const humans = (data.humans ?? []).map(h => ` @${h.name}`).join('\n');
|
|
78
|
+
return { content: [{ type: 'text', text: `Channels:\n${channels}\n\nAgents:\n${agents}\n\nHumans:\n${humans}` }] };
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ── list_tasks ────────────────────────────────────────────────────────────────
|
|
82
|
+
server.tool('list_tasks', 'List tasks in a channel', {
|
|
83
|
+
channel: z.string().describe('Target: #channel-name'),
|
|
84
|
+
status: z.enum(['all', 'todo', 'in_progress', 'in_review', 'done']).optional(),
|
|
85
|
+
}, async ({ channel, status }) => {
|
|
86
|
+
const params = new URLSearchParams({ channel, status: status ?? 'all' });
|
|
87
|
+
const data = await api('GET', `/tasks?${params}`);
|
|
88
|
+
const tasks = data.tasks ?? [];
|
|
89
|
+
if (tasks.length === 0) return { content: [{ type: 'text', text: 'No tasks found.' }] };
|
|
90
|
+
|
|
91
|
+
const text = tasks.map(t =>
|
|
92
|
+
`#${t.taskNumber} [${t.status}] ${t.title}` +
|
|
93
|
+
(t.claimedByName ? ` (claimed by ${t.claimedByName})` : '')
|
|
94
|
+
).join('\n');
|
|
95
|
+
return { content: [{ type: 'text', text }] };
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ── create_tasks ──────────────────────────────────────────────────────────────
|
|
99
|
+
server.tool('create_tasks', 'Create one or more tasks in a channel', {
|
|
100
|
+
channel: z.string().describe('Target: #channel-name'),
|
|
101
|
+
tasks: z.array(z.object({ title: z.string() })).describe('Array of tasks to create'),
|
|
102
|
+
}, async ({ channel, tasks }) => {
|
|
103
|
+
const data = await api('POST', '/tasks', { channel, tasks });
|
|
104
|
+
const created = (data.tasks ?? []).map(t => `#${t.taskNumber} ${t.title}`).join('\n');
|
|
105
|
+
return { content: [{ type: 'text', text: `Created:\n${created}` }] };
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ── claim_tasks ───────────────────────────────────────────────────────────────
|
|
109
|
+
server.tool('claim_tasks', 'Claim one or more tasks to work on', {
|
|
110
|
+
channel: z.string().describe('Target: #channel-name'),
|
|
111
|
+
task_numbers: z.array(z.number()).optional().describe('Task numbers to claim'),
|
|
112
|
+
message_ids: z.array(z.string()).optional().describe('Short message IDs to claim'),
|
|
113
|
+
}, async ({ channel, task_numbers, message_ids }) => {
|
|
114
|
+
const data = await api('POST', '/tasks/claim', { channel, task_numbers, message_ids });
|
|
115
|
+
const results = (data.results ?? []).map(r =>
|
|
116
|
+
`#${r.taskNumber}: ${r.success ? 'claimed' : `failed (${r.reason})`}`
|
|
117
|
+
).join('\n');
|
|
118
|
+
return { content: [{ type: 'text', text: results }] };
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ── unclaim_task ──────────────────────────────────────────────────────────────
|
|
122
|
+
server.tool('unclaim_task', 'Release a claimed task', {
|
|
123
|
+
channel: z.string().describe('Target: #channel-name'),
|
|
124
|
+
task_number: z.number().describe('Task number to unclaim'),
|
|
125
|
+
}, async ({ channel, task_number }) => {
|
|
126
|
+
await api('POST', '/tasks/unclaim', { channel, task_number });
|
|
127
|
+
return { content: [{ type: 'text', text: `Task #${task_number} unclaimed.` }] };
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ── update_task_status ────────────────────────────────────────────────────────
|
|
131
|
+
server.tool('update_task_status', 'Update the status of a task', {
|
|
132
|
+
channel: z.string().describe('Target: #channel-name'),
|
|
133
|
+
task_number: z.number().describe('Task number'),
|
|
134
|
+
status: z.enum(['todo', 'in_progress', 'in_review', 'done']).describe('New status'),
|
|
135
|
+
}, async ({ channel, task_number, status }) => {
|
|
136
|
+
await api('POST', '/tasks/update-status', { channel, task_number, status });
|
|
137
|
+
return { content: [{ type: 'text', text: `Task #${task_number} → ${status}` }] };
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ── list_memory ───────────────────────────────────────────────────────────────
|
|
141
|
+
server.tool('list_memory', 'List all memory files stored for this agent on the server', {}, async () => {
|
|
142
|
+
const data = await api('GET', '/memory');
|
|
143
|
+
const files = data.files ?? [];
|
|
144
|
+
if (files.length === 0) return { content: [{ type: 'text', text: 'No memory files yet.' }] };
|
|
145
|
+
return { content: [{ type: 'text', text: files.map(f => f.path).join('\n') }] };
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ── read_memory ───────────────────────────────────────────────────────────────
|
|
149
|
+
server.tool('read_memory', 'Read a memory file by path (e.g. "MEMORY.md" or "notes/work-log.md")', {
|
|
150
|
+
path: z.string().describe('File path, e.g. "MEMORY.md" or "notes/channels.md"'),
|
|
151
|
+
}, async ({ path }) => {
|
|
152
|
+
try {
|
|
153
|
+
const data = await api('GET', `/memory?path=${encodeURIComponent(path)}`);
|
|
154
|
+
return { content: [{ type: 'text', text: data.content }] };
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (err.message.includes('404')) return { content: [{ type: 'text', text: `(empty — ${path} has no content yet)` }] };
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ── write_memory ──────────────────────────────────────────────────────────────
|
|
162
|
+
server.tool('write_memory', 'Write or update a memory file (full content replace)', {
|
|
163
|
+
path: z.string().describe('File path, e.g. "MEMORY.md" or "notes/work-log.md"'),
|
|
164
|
+
content: z.string().describe('Full file content to store'),
|
|
165
|
+
}, async ({ path, content }) => {
|
|
166
|
+
await api('PUT', `/memory?path=${encodeURIComponent(path)}`, { content });
|
|
167
|
+
return { content: [{ type: 'text', text: `Saved ${path}` }] };
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ── read_file_base64 ──────────────────────────────────────────────────────────
|
|
171
|
+
// Agent 需要在本机读取图片文件内容,转为 base64 后上传服务器
|
|
172
|
+
server.tool('read_file_base64',
|
|
173
|
+
'读取本机文件内容,返回 base64 编码。用于上传图片到服务器。',
|
|
174
|
+
{
|
|
175
|
+
file_path: z.string().describe('本机文件的绝对路径'),
|
|
176
|
+
},
|
|
177
|
+
async ({ file_path }) => {
|
|
178
|
+
const { readFileSync } = await import('fs');
|
|
179
|
+
const data = readFileSync(file_path).toString('base64');
|
|
180
|
+
return { content: [{ type: 'text', text: data }] };
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// ── upload_image ──────────────────────────────────────────────────────────────
|
|
185
|
+
server.tool('upload_image',
|
|
186
|
+
'将本机图片文件(绝对路径)上传到服务器,返回公开可访问的 URL。用于将 QR 码截图等发给用户。',
|
|
187
|
+
{
|
|
188
|
+
file_path: z.string().describe('本机图片文件的绝对路径'),
|
|
189
|
+
},
|
|
190
|
+
async ({ file_path }) => {
|
|
191
|
+
const { readFileSync } = await import('fs');
|
|
192
|
+
const { extname, basename } = await import('path');
|
|
193
|
+
const data = readFileSync(file_path).toString('base64');
|
|
194
|
+
const filename = basename(file_path);
|
|
195
|
+
const result = await api('POST', '/upload', { filename, data });
|
|
196
|
+
return { content: [{ type: 'text', text: `上传成功: ${result.url}` }] };
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// ── xhs_save_account ──────────────────────────────────────────────────────────
|
|
201
|
+
server.tool('xhs_save_account',
|
|
202
|
+
'保存小红书账号的登录 cookies 到服务器,供后续发布使用。',
|
|
203
|
+
{
|
|
204
|
+
feishu_user_id: z.string().describe('飞书用户 ID(消息中的 sender open_id)'),
|
|
205
|
+
account_name: z.string().describe('账号备注名,如小红书昵称'),
|
|
206
|
+
cookies: z.array(z.object({
|
|
207
|
+
name: z.string(),
|
|
208
|
+
value: z.string(),
|
|
209
|
+
domain: z.string().optional(),
|
|
210
|
+
path: z.string().optional(),
|
|
211
|
+
})).describe('Cookie 数组'),
|
|
212
|
+
},
|
|
213
|
+
async ({ feishu_user_id, account_name, cookies }) => {
|
|
214
|
+
const result = await api('POST', '/xhs/accounts', {
|
|
215
|
+
feishuUserId: feishu_user_id,
|
|
216
|
+
accountName: account_name,
|
|
217
|
+
cookies,
|
|
218
|
+
});
|
|
219
|
+
return { content: [{ type: 'text', text: `账号已保存,id=${result.id}` }] };
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// ── xhs_get_cookies ───────────────────────────────────────────────────────────
|
|
224
|
+
server.tool('xhs_get_cookies',
|
|
225
|
+
'获取已保存的小红书账号 cookies,发布前调用。',
|
|
226
|
+
{},
|
|
227
|
+
async () => {
|
|
228
|
+
try {
|
|
229
|
+
const result = await api('GET', `/xhs/accounts/latest?feishuUserId=any`);
|
|
230
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
231
|
+
} catch (err) {
|
|
232
|
+
if (err.message.includes('404')) return { content: [{ type: 'text', text: 'NO_ACCOUNT: 尚未绑定小红书账号,请先提供 cookies' }] };
|
|
233
|
+
throw err;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// ── start ─────────────────────────────────────────────────────────────────────
|
|
239
|
+
const transport = new StdioServerTransport();
|
|
240
|
+
await server.connect(transport);
|
|
241
|
+
console.error(`[chat-bridge] MCP Server started (agentId=${AGENT_ID})`);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
|
|
4
|
+
const RECONNECT_INITIAL = 1000;
|
|
5
|
+
const RECONNECT_MAX = 30000;
|
|
6
|
+
|
|
7
|
+
export class DaemonConnection {
|
|
8
|
+
constructor({ serverUrl, machineApiKey, onMessage }) {
|
|
9
|
+
this.serverUrl = serverUrl.replace(/^http/, 'ws');
|
|
10
|
+
this.machineApiKey = machineApiKey;
|
|
11
|
+
this.onMessage = onMessage;
|
|
12
|
+
this.ws = null;
|
|
13
|
+
this.reconnectDelay = RECONNECT_INITIAL;
|
|
14
|
+
this.stopped = false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
connect() {
|
|
18
|
+
const url = `${this.serverUrl}/daemon/connect?key=${this.machineApiKey}`;
|
|
19
|
+
console.log(`[Connection] Connecting to ${url}`);
|
|
20
|
+
this.ws = new WebSocket(url);
|
|
21
|
+
|
|
22
|
+
this.ws.on('open', () => {
|
|
23
|
+
console.log('[Connection] Connected');
|
|
24
|
+
this.reconnectDelay = RECONNECT_INITIAL;
|
|
25
|
+
this._sendReady();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
this.ws.on('message', (raw) => {
|
|
29
|
+
let msg;
|
|
30
|
+
try { msg = JSON.parse(raw.toString()); }
|
|
31
|
+
catch { return; }
|
|
32
|
+
this.onMessage(msg);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
this.ws.on('close', (code) => {
|
|
36
|
+
console.log(`[Connection] Disconnected (code=${code})`);
|
|
37
|
+
if (!this.stopped) this._scheduleReconnect();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
this.ws.on('error', (err) => {
|
|
41
|
+
console.error('[Connection] Error:', err.message);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
send(msg) {
|
|
46
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
47
|
+
this.ws.send(JSON.stringify(msg));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
stop() {
|
|
52
|
+
this.stopped = true;
|
|
53
|
+
this.ws?.close();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_sendReady() {
|
|
57
|
+
this.send({
|
|
58
|
+
type: 'ready',
|
|
59
|
+
hostname: os.hostname(),
|
|
60
|
+
os: `${os.platform()} ${os.arch()}`,
|
|
61
|
+
runtimes: ['claude'],
|
|
62
|
+
daemonVersion: '0.1.0',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_scheduleReconnect() {
|
|
67
|
+
console.log(`[Connection] Reconnecting in ${this.reconnectDelay}ms...`);
|
|
68
|
+
setTimeout(() => this.connect(), this.reconnectDelay);
|
|
69
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, RECONNECT_MAX);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
const BASE_PROMPT = (displayName, name, description, agentId) => `\
|
|
2
|
+
You are ${displayName} (username: ${name}), a persistent AI agent in a team collaboration platform.
|
|
3
|
+
${description ? `\nYour role: ${description}\n` : ''}
|
|
4
|
+
## Core Rules
|
|
5
|
+
|
|
6
|
+
- You ONLY communicate using MCP tools. Never output plain text as a response to messages.
|
|
7
|
+
- You are persistent — you stay running between messages and maintain memory across conversations.
|
|
8
|
+
- Always check for new messages first, then respond.
|
|
9
|
+
|
|
10
|
+
## Startup Sequence
|
|
11
|
+
|
|
12
|
+
When you start or resume:
|
|
13
|
+
1. Call \`mcp__chat__read_memory\` with path \`MEMORY.md\` to load your memory index
|
|
14
|
+
2. Based on the index, call \`mcp__chat__read_memory\` for any notes files relevant to current work
|
|
15
|
+
3. Call \`mcp__chat__check_messages\` to see pending messages
|
|
16
|
+
4. If there are messages, process and respond to them
|
|
17
|
+
5. If no messages, call \`mcp__chat__list_server\` to understand your environment, then wait
|
|
18
|
+
|
|
19
|
+
## Sending Messages
|
|
20
|
+
|
|
21
|
+
Use \`mcp__chat__send_message\` with a \`target\` field:
|
|
22
|
+
- \`#channel-name\` — send to a channel
|
|
23
|
+
- \`dm:@agentName\` — send a DM to another agent
|
|
24
|
+
- \`#channel-name:shortMsgId\` — reply in a thread (shortMsgId = first 8 chars of message ID)
|
|
25
|
+
|
|
26
|
+
## Task Workflow
|
|
27
|
+
|
|
28
|
+
Tasks flow through these statuses: \`todo → in_progress → in_review → done\`
|
|
29
|
+
|
|
30
|
+
- Use \`mcp__chat__list_tasks\` to see available tasks
|
|
31
|
+
- Use \`mcp__chat__claim_tasks\` to claim a task before working on it
|
|
32
|
+
- Use \`mcp__chat__update_task_status\` to move tasks forward
|
|
33
|
+
- Use \`mcp__chat__unclaim_task\` if you cannot complete a task
|
|
34
|
+
|
|
35
|
+
## Filesystem Access
|
|
36
|
+
|
|
37
|
+
- You have full filesystem access via Bash, Read, Write, Edit tools.
|
|
38
|
+
- The shared web server public directory is: \`/home/ubuntu/lightcone/public/\`
|
|
39
|
+
- Write web app files directly there so they are served immediately.
|
|
40
|
+
- Your current working directory is a temporary workspace for code, scripts, and build artifacts.
|
|
41
|
+
|
|
42
|
+
## Memory
|
|
43
|
+
|
|
44
|
+
Your memory is stored on the server and persists across machines and restarts. Use these MCP tools:
|
|
45
|
+
|
|
46
|
+
- \`mcp__chat__read_memory({ path })\` — read a memory file (e.g. \`"MEMORY.md"\`, \`"notes/work-log.md"\`)
|
|
47
|
+
- \`mcp__chat__write_memory({ path, content })\` — save a memory file (full replace)
|
|
48
|
+
- \`mcp__chat__list_memory()\` — list all your memory files
|
|
49
|
+
|
|
50
|
+
**MEMORY.md is your index.** Keep it as a concise table of contents pointing to notes files.
|
|
51
|
+
After every significant interaction, update the relevant notes file and keep MEMORY.md current.
|
|
52
|
+
Do NOT store memory as local files — always use \`write_memory\` so it persists server-side.
|
|
53
|
+
|
|
54
|
+
## Message Format Reference
|
|
55
|
+
|
|
56
|
+
When you receive a message via stdin, it looks like:
|
|
57
|
+
\`\`\`json
|
|
58
|
+
{"type": "new_message", "message": {"channel_name": "general", "sender_name": "Alice", "content": "..."}}
|
|
59
|
+
\`\`\`
|
|
60
|
+
|
|
61
|
+
Your agent ID is: ${agentId}
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
// ── 专属提示词 ────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
const DATA_FETCHER_PROMPT = `
|
|
67
|
+
## 你的专属职责:获取数据
|
|
68
|
+
|
|
69
|
+
你是内容发布流水线的第一环。当 #content-publish 频道收到用户的职位推广需求时,你负责从职位数据库查询数据并输出结构化结果。
|
|
70
|
+
|
|
71
|
+
### 工作流程
|
|
72
|
+
|
|
73
|
+
1. 监听 #content-publish 频道的用户消息
|
|
74
|
+
2. 识别用户需求(如"推广 Java 后端实习岗位"、"帮我发一下腾讯的产品经理职位")
|
|
75
|
+
3. 使用 MySQL MCP 工具查询职位数据:
|
|
76
|
+
- \`mcp__mysql__search_jobs\` — 按关键词/公司/类型/地点搜索
|
|
77
|
+
- \`mcp__mysql__get_job_detail\` — 获取完整职位详情
|
|
78
|
+
- \`mcp__mysql__list_jobs\` — 浏览最新职位
|
|
79
|
+
4. 如果找到多个职位,选最相关的 1 个(或让用户确认)
|
|
80
|
+
5. 将完整职位数据以结构化格式发到频道,并 @copywriter 接力
|
|
81
|
+
|
|
82
|
+
### 输出格式(发消息时必须包含此块)
|
|
83
|
+
|
|
84
|
+
\`\`\`
|
|
85
|
+
【职位数据已就绪】
|
|
86
|
+
job_id: xxx
|
|
87
|
+
职位名称: xxx
|
|
88
|
+
公司: xxx
|
|
89
|
+
薪资: xxx
|
|
90
|
+
地点: xxx
|
|
91
|
+
类型: xxx
|
|
92
|
+
学历: xxx
|
|
93
|
+
标签: xxx
|
|
94
|
+
职位描述:
|
|
95
|
+
xxx
|
|
96
|
+
【数据结束】
|
|
97
|
+
\`\`\`
|
|
98
|
+
|
|
99
|
+
然后发一条消息告知 @copywriter 可以开始写文案了。
|
|
100
|
+
|
|
101
|
+
### 注意事项
|
|
102
|
+
|
|
103
|
+
- 如果数据库没有匹配职位,告知用户并请求更多信息
|
|
104
|
+
- 职位描述很长时,保留全部内容(写手需要完整信息)
|
|
105
|
+
- 不要自己写文案,数据整理完就交给写手
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
const COPYWRITER_PROMPT = `
|
|
109
|
+
## 你的专属职责:写手
|
|
110
|
+
|
|
111
|
+
你是内容发布流水线的第二环。当获取数据 Agent 发来职位数据后,你负责撰写一篇小红书风格的职位推广图文。
|
|
112
|
+
|
|
113
|
+
### 工作流程
|
|
114
|
+
|
|
115
|
+
1. 监听 #content-publish 频道,等待包含【职位数据已就绪】标记的消息
|
|
116
|
+
2. 仔细阅读职位详情,提炼核心卖点
|
|
117
|
+
3. 撰写小红书风格图文(风格活泼、有吸引力、适合年轻求职者)
|
|
118
|
+
4. 将文案发到频道,并 @designer 接力
|
|
119
|
+
|
|
120
|
+
### 文案结构要求
|
|
121
|
+
|
|
122
|
+
**标题**(20字以内,吸引眼球)
|
|
123
|
+
- 示例:"字节跳动|产品实习,一起做改变世界的产品!"
|
|
124
|
+
|
|
125
|
+
**正文**(300-500字)
|
|
126
|
+
- 开头用emoji吸引注意
|
|
127
|
+
- 公司亮点(2-3条)
|
|
128
|
+
- 职位核心职责(3-4条,简洁)
|
|
129
|
+
- 岗位要求(2-3条关键要求)
|
|
130
|
+
- 薪资/福利亮点
|
|
131
|
+
- 结尾引导互动("评论区扣1,私信发简历~")
|
|
132
|
+
|
|
133
|
+
**话题标签**(8-12个)
|
|
134
|
+
- 固定包含:#实习招聘 #求职 #校招
|
|
135
|
+
- 根据职位添加:公司名、城市、职位类型等
|
|
136
|
+
|
|
137
|
+
**图片设计要点**(给设计师的指引,简洁列出核心视觉信息)
|
|
138
|
+
- 最重要的 5-6 个信息点(公司名、职位名、薪资、地点、1-2个核心亮点)
|
|
139
|
+
- 建议配色或风格(可选)
|
|
140
|
+
|
|
141
|
+
### 输出格式
|
|
142
|
+
|
|
143
|
+
\`\`\`
|
|
144
|
+
【文案已就绪】
|
|
145
|
+
---标题---
|
|
146
|
+
[标题内容]
|
|
147
|
+
---正文---
|
|
148
|
+
[正文内容]
|
|
149
|
+
---标签---
|
|
150
|
+
[话题标签]
|
|
151
|
+
---设计要点---
|
|
152
|
+
[给设计师的核心信息和视觉指引]
|
|
153
|
+
【文案结束】
|
|
154
|
+
\`\`\`
|
|
155
|
+
|
|
156
|
+
然后 @designer 可以开始制图了。
|
|
157
|
+
|
|
158
|
+
### 注意事项
|
|
159
|
+
|
|
160
|
+
- 不要夸大薪资或福利,以数据库中的真实信息为准
|
|
161
|
+
- 风格活泼但不浮夸,适合 Z 世代求职者
|
|
162
|
+
- 不要做设计,文字工作完成后立即交给设计师
|
|
163
|
+
`;
|
|
164
|
+
|
|
165
|
+
const DESIGNER_PROMPT = `
|
|
166
|
+
## 你的专属职责:设计师
|
|
167
|
+
|
|
168
|
+
你是内容发布流水线的第三环。当写手发来文案后,你负责**写代码**生成一张精美的小红书职位推广图片。
|
|
169
|
+
|
|
170
|
+
### 工作流程
|
|
171
|
+
|
|
172
|
+
1. 监听 #content-publish 频道,等待包含【文案已就绪】标记的消息
|
|
173
|
+
2. 提取文案中的核心信息和设计要点
|
|
174
|
+
3. 编写 Node.js 代码,使用 canvas 或 Puppeteer(渲染 HTML)生成图片
|
|
175
|
+
4. 执行代码,生成图片文件到工作目录
|
|
176
|
+
5. 将图片文件路径发到频道,并 @publisher 接力
|
|
177
|
+
|
|
178
|
+
### 图片规格
|
|
179
|
+
|
|
180
|
+
- 尺寸:1080×1440px(小红书竖版标准)
|
|
181
|
+
- 格式:PNG
|
|
182
|
+
- 必须包含的信息:公司名、职位名、薪资(或"薪资面议")、工作地点、1-2个核心亮点、招聘字样
|
|
183
|
+
|
|
184
|
+
### 技术方案(推荐 Puppeteer 渲染 HTML)
|
|
185
|
+
|
|
186
|
+
\`\`\`javascript
|
|
187
|
+
// 安装:npm install puppeteer(如果没有)
|
|
188
|
+
// 用 HTML + CSS 设计排版,然后截图
|
|
189
|
+
|
|
190
|
+
import puppeteer from 'puppeteer';
|
|
191
|
+
import { writeFileSync } from 'fs';
|
|
192
|
+
|
|
193
|
+
const browser = await puppeteer.launch({ args: ['--no-sandbox'] });
|
|
194
|
+
const page = await browser.newPage();
|
|
195
|
+
await page.setViewport({ width: 1080, height: 1440 });
|
|
196
|
+
await page.setContent(\`<!-- HTML模板 -->\`);
|
|
197
|
+
await page.screenshot({ path: '/tmp/job-post-xxx.png', fullPage: false });
|
|
198
|
+
await browser.close();
|
|
199
|
+
\`\`\`
|
|
200
|
+
|
|
201
|
+
### 设计风格指引
|
|
202
|
+
|
|
203
|
+
- 现代简洁,色彩鲜明(可用渐变背景)
|
|
204
|
+
- 公司名和职位名字号最大,视觉层级清晰
|
|
205
|
+
- 薪资用醒目颜色(金色或红色)标注
|
|
206
|
+
- 底部可加"扫码投递"占位或品牌标识区域
|
|
207
|
+
- 整体风格参考小红书上流行的求职帖子
|
|
208
|
+
|
|
209
|
+
### 输出格式
|
|
210
|
+
|
|
211
|
+
执行完代码后,发送:
|
|
212
|
+
\`\`\`
|
|
213
|
+
【图片已生成】
|
|
214
|
+
file: /tmp/job-post-xxx.png
|
|
215
|
+
【图片结束】
|
|
216
|
+
\`\`\`
|
|
217
|
+
|
|
218
|
+
然后 @publisher 可以开始发布了。
|
|
219
|
+
|
|
220
|
+
### 注意事项
|
|
221
|
+
|
|
222
|
+
- **必须真正执行代码**生成文件,不能只写代码不运行
|
|
223
|
+
- 如果 puppeteer 没有安装,先运行 \`npm install puppeteer\`
|
|
224
|
+
- 图片生成失败时告知频道并说明原因
|
|
225
|
+
- 不要发布,设计完交给发布实习生
|
|
226
|
+
`;
|
|
227
|
+
|
|
228
|
+
const PUBLISHER_PROMPT = `
|
|
229
|
+
## 你的专属职责:发布实习生
|
|
230
|
+
|
|
231
|
+
你是内容发布流水线的最后一环。你有两个主要职责:
|
|
232
|
+
1. **绑定小红书账号**:引导用户完成扫码登录,保存 cookies
|
|
233
|
+
2. **发布内容**:将设计师生成的图片和写手的文案发布到小红书
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
### 职责一:绑定小红书账号
|
|
238
|
+
|
|
239
|
+
当用户说"绑定小红书账号"、"登录小红书"等,执行以下步骤:
|
|
240
|
+
|
|
241
|
+
1. 用 Chrome DevTools MCP 打开小红书登录页
|
|
242
|
+
\`\`\`
|
|
243
|
+
mcp__chrome-devtools__navigate_page(url: "https://www.xiaohongshu.com/login")
|
|
244
|
+
\`\`\`
|
|
245
|
+
2. 等待 QR 码出现后截图,保存到本地
|
|
246
|
+
\`\`\`
|
|
247
|
+
mcp__chrome-devtools__take_screenshot(filePath: "/tmp/xhs-qr-xxx.png")
|
|
248
|
+
\`\`\`
|
|
249
|
+
3. 将截图上传到服务器,获取公开 URL
|
|
250
|
+
\`\`\`
|
|
251
|
+
mcp__chat__upload_image(file_path: "/tmp/xhs-qr-xxx.png")
|
|
252
|
+
\`\`\`
|
|
253
|
+
4. 在频道发送 URL,告知用户用小红书 App 扫码
|
|
254
|
+
5. 每 5 秒轮询一次登录状态(检查页面 URL 是否跳转或检查 cookies)
|
|
255
|
+
6. 登录成功后,提取 cookies 并保存:
|
|
256
|
+
\`\`\`
|
|
257
|
+
mcp__chrome-devtools__evaluate_script(function: "() => document.cookie")
|
|
258
|
+
mcp__chat__xhs_save_account(feishu_user_id: "xxx", account_name: "xxx", cookies: [...])
|
|
259
|
+
\`\`\`
|
|
260
|
+
7. 告知用户绑定成功
|
|
261
|
+
|
|
262
|
+
**注意**:从发消息的 sender 信息中获取飞书用户 ID,保存时关联到该用户。
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
### 职责二:发布内容
|
|
267
|
+
|
|
268
|
+
当频道收到包含【图片已生成】标记的消息时,执行发布流程:
|
|
269
|
+
|
|
270
|
+
1. 从消息中提取图片路径(file: /tmp/xxx.png)
|
|
271
|
+
2. 从频道历史中找到写手的【文案已就绪】消息,提取标题、正文、标签
|
|
272
|
+
3. 获取当前用户的小红书 cookies:
|
|
273
|
+
\`\`\`
|
|
274
|
+
mcp__chat__xhs_get_cookies(feishu_user_id: "xxx")
|
|
275
|
+
\`\`\`
|
|
276
|
+
4. 如果没有绑定账号,提示用户先绑定
|
|
277
|
+
5. 导航到小红书创作者中心并注入 cookies:
|
|
278
|
+
\`\`\`
|
|
279
|
+
mcp__chrome-devtools__navigate_page(url: "https://creator.xiaohongshu.com")
|
|
280
|
+
// 注入 cookies
|
|
281
|
+
mcp__chrome-devtools__evaluate_script(function: "() => { /* set cookies */ }")
|
|
282
|
+
\`\`\`
|
|
283
|
+
6. 上传图片、填写标题和正文、添加话题标签、点击发布
|
|
284
|
+
7. 获取发布后的帖子 URL,发回频道
|
|
285
|
+
|
|
286
|
+
**发布步骤细节**:
|
|
287
|
+
- 导航到发布页面
|
|
288
|
+
- 点击"上传图片"按钮,使用 upload_file 上传图片
|
|
289
|
+
- 填写标题(限20字)
|
|
290
|
+
- 填写正文
|
|
291
|
+
- 添加话题标签
|
|
292
|
+
- 点击发布按钮
|
|
293
|
+
- 等待成功提示,抓取帖子链接
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
### 注意事项
|
|
298
|
+
|
|
299
|
+
- cookies 可能过期,发布前先访问个人主页验证是否仍有效
|
|
300
|
+
- 若 cookies 失效,在频道告知用户需要重新绑定账号
|
|
301
|
+
- 发布频率不要太快,每次发布后等待至少 5 秒
|
|
302
|
+
- 小红书页面结构可能变化,如果选择器失效,用 take_snapshot 查看当前页面结构再调整
|
|
303
|
+
`;
|
|
304
|
+
|
|
305
|
+
// ── 主函数 ────────────────────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
export function buildSystemPrompt(config, agentId) {
|
|
308
|
+
const { name, displayName, description } = config;
|
|
309
|
+
|
|
310
|
+
const base = BASE_PROMPT(displayName, name, description, agentId);
|
|
311
|
+
|
|
312
|
+
const rolePrompt = {
|
|
313
|
+
'data-fetcher': DATA_FETCHER_PROMPT,
|
|
314
|
+
'copywriter': COPYWRITER_PROMPT,
|
|
315
|
+
'designer': DESIGNER_PROMPT,
|
|
316
|
+
'publisher': PUBLISHER_PROMPT,
|
|
317
|
+
}[name] ?? '';
|
|
318
|
+
|
|
319
|
+
return base + rolePrompt;
|
|
320
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import { DaemonConnection } from './connection.js';
|
|
4
|
+
import { AgentManager } from './agent-manager.js';
|
|
5
|
+
|
|
6
|
+
// ── CLI args ──────────────────────────────────────────────────────────────────
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
let cliServerUrl = '';
|
|
9
|
+
let cliApiKey = '';
|
|
10
|
+
|
|
11
|
+
for (let i = 0; i < args.length; i++) {
|
|
12
|
+
if (args[i] === '--server-url' && args[i + 1]) cliServerUrl = args[++i];
|
|
13
|
+
if (args[i] === '--api-key' && args[i + 1]) cliApiKey = args[++i];
|
|
14
|
+
if (args[i] === '--help' || args[i] === '-h') {
|
|
15
|
+
console.log('Usage: lightcone-daemon --server-url <url> --api-key <key>');
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SERVER_URL = cliServerUrl || process.env.SERVER_URL || 'http://localhost:8779';
|
|
21
|
+
const MACHINE_API_KEY = cliApiKey || process.env.MACHINE_API_KEY || '';
|
|
22
|
+
|
|
23
|
+
if (!MACHINE_API_KEY) {
|
|
24
|
+
console.error('Error: API key is required.');
|
|
25
|
+
console.error('Usage: lightcone-daemon --server-url <url> --api-key <key>');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(`[Daemon] Server: ${SERVER_URL}`);
|
|
30
|
+
|
|
31
|
+
// ── Start ─────────────────────────────────────────────────────────────────────
|
|
32
|
+
const agentManager = new AgentManager({ serverUrl: SERVER_URL, machineApiKey: MACHINE_API_KEY });
|
|
33
|
+
|
|
34
|
+
const connection = new DaemonConnection({
|
|
35
|
+
serverUrl: SERVER_URL,
|
|
36
|
+
machineApiKey: MACHINE_API_KEY,
|
|
37
|
+
onMessage: (msg) => agentManager.handle(msg, connection),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
connection.connect();
|
|
41
|
+
|
|
42
|
+
process.on('SIGINT', () => { connection.stop(); agentManager.stopAll(); process.exit(0); });
|
|
43
|
+
process.on('SIGTERM', () => { connection.stop(); agentManager.stopAll(); process.exit(0); });
|