chore: consolidate admin cleanup and colima config

This commit is contained in:
john
2026-06-28 08:20:43 +08:00
parent fffa1f7c7d
commit dcf8fd1ff9
19 changed files with 426 additions and 89 deletions
+59 -29
View File
@@ -1,16 +1,18 @@
/**
* GoosedExecutor — runs a skill via a one-shot goosed session.
* GoosedExecutor — runs a skill via a goosed session.
*
* Session lifecycle:
* 1. POST /agent/start { messages: [{ role: 'user', content: prompt }] }
* 2. GET /sessions/:id/events (SSE) → collect until Finish event or timeout
* 3. DELETE /sessions/:id (best-effort cleanup)
* 1. POST /agent/start { working_dir }
* 2. POST /sessions/:id/reply { request_id: UUID, user_message: {...} }
* 3. GET /sessions/:id/events (SSE) → collect until Finish event or timeout
* 4. DELETE /sessions/:id (best-effort cleanup)
*
* Dual-node: pickGoosedUrl() round-robins across configured nodes.
* Protocol: HTTPS with self-signed cert (rejectUnauthorized: false).
*/
import https from 'node:https';
import crypto from 'node:crypto';
import { goosedFetch, pickGoosedUrl } from '../goosed-client.mjs';
const SESSION_TIMEOUT_MS = Number(process.env.SKILL_GOOSED_TIMEOUT_MS ?? 120_000);
@@ -18,19 +20,14 @@ const tlsAgent = new https.Agent({ rejectUnauthorized: false });
export async function invokeGoosedSkill(skill, input) {
const node = pickGoosedUrl();
const prompt = buildPrompt(skill, input);
const systemPrompt = buildSystemPrompt(skill, input);
const userPrompt = buildUserPrompt(skill, input);
// 1. Start session
// 1. Start session (no messages yet)
const startRes = await goosedFetch('/agent/start', {
method: 'POST',
node,
body: {
version: '1.0.0',
session_id: null,
...(systemPrompt ? { system_prompt_override: systemPrompt } : {}),
messages: [{ role: 'user', content: userPrompt }],
working_dir: process.env.GOOSE_SKILL_WORKING_DIR ?? '/tmp',
},
});
@@ -41,12 +38,42 @@ export async function invokeGoosedSkill(skill, input) {
);
}
const sessionId = startRes.body?.session_id ?? startRes.body?.id;
const sessionId = startRes.body?.id ?? startRes.body?.session_id;
if (!sessionId) {
throw new Error(`goosed did not return a session_id: ${JSON.stringify(startRes.body)}`);
throw new Error(`goosed did not return a session id: ${JSON.stringify(startRes.body)}`);
}
// 2. Stream events and collect result
// 1b. Set the LLM provider on the session
const goosedProvider = process.env.GOOSE_PROVIDER ?? process.env.GOOSED_PROVIDER ?? 'custom_deepseek';
const goosedModel = process.env.GOOSE_MODEL ?? process.env.GOOSED_MODEL;
const providerBody = { session_id: sessionId, provider: goosedProvider };
if (goosedModel) providerBody.model = goosedModel;
await goosedFetch('/agent/update_provider', { method: 'POST', node, body: providerBody });
// 2. Send the prompt via reply
const requestId = crypto.randomUUID();
const replyRes = await goosedFetch(`/sessions/${sessionId}/reply`, {
method: 'POST',
node,
body: {
request_id: requestId,
user_message: {
role: 'user',
created: Math.floor(Date.now() / 1000),
metadata: { userVisible: true, agentVisible: true },
content: [{ type: 'text', text: prompt }],
},
},
});
if (!replyRes.ok) {
throw Object.assign(
new Error(`goosed /sessions/${sessionId}/reply failed (${replyRes.status})`),
{ status: replyRes.status, upstream: replyRes.body },
);
}
// 3. Stream SSE events and collect result
try {
return await collectSessionResult(node, sessionId);
} finally {
@@ -54,14 +81,22 @@ export async function invokeGoosedSkill(skill, input) {
}
}
function buildSystemPrompt(skill, input) {
if (skill.runtime?.config?.system_prompt) return skill.runtime.config.system_prompt;
return null;
}
function buildUserPrompt(skill, input) {
function buildPrompt(skill, input) {
if (input.design_md_content) {
return `You are a UI generation assistant. Follow this design system exactly:\n\n${input.design_md_content}\n\nTask: ${input.task ?? input.prompt ?? ''}`;
return `You are a UI generation assistant. Your output must be a complete, self-contained HTML page — not a component, not a snippet, not a code block with backticks. Output raw HTML only, starting with <!DOCTYPE html>.
Apply the following design system exactly. Match the colors, typography, spacing, border styles, shadows, and interaction patterns described:
${input.design_md_content}
Task: ${input.task ?? input.prompt ?? ''}
Requirements:
- Output a single complete HTML file with <html>, <head>, <body> and all inline <style> — no external stylesheets or JS frameworks
- The page should feel like a real product page from this brand, not a generic template
- Use the exact color values, font sizes, border-radius, shadow formulas from the design system above
- Include realistic placeholder content relevant to the task
- The page must be visually complete and ready to render in a browser`;
}
return input.task ?? input.prompt ?? JSON.stringify(input);
}
@@ -80,13 +115,10 @@ function collectSessionResult(node, sessionId) {
path: url.pathname,
method: 'GET',
headers: { 'X-Secret-Key': secret, Accept: 'text/event-stream' },
agent: url.protocol === 'https:' ? tlsAgent : undefined,
rejectUnauthorized: false,
agent: tlsAgent,
};
const proto = url.protocol === 'https:' ? https : (() => { throw new Error('http not supported for goosed'); })();
const req = proto.request(options, (res) => {
const req = https.request(options, (res) => {
if (res.statusCode !== 200) {
res.resume();
return reject(new Error(`SSE stream returned ${res.statusCode}`));
@@ -144,14 +176,12 @@ function collectSessionResult(node, sessionId) {
}
function extractText(event) {
// Handle goosed SSE event shapes
if (event?.type === 'Message' && event?.message?.role === 'assistant') {
return (event.message.content ?? [])
.filter((c) => c.type === 'text')
.map((c) => c.text)
.join('');
}
// Simpler delta format
if (event?.delta?.text) return event.delta.text;
if (typeof event?.content === 'string') return event.content;
return '';
+35 -17
View File
@@ -46,28 +46,46 @@ export function goosedUrls() {
/**
* Make a JSON request to a goosed node.
* Returns { ok, status, body } — never throws on HTTP errors, only on network failure.
* Uses https.request directly so rejectUnauthorized: false works for self-signed certs.
*/
export async function goosedFetch(path, { method = 'GET', body, node } = {}) {
export function goosedFetch(path, { method = 'GET', body, node } = {}) {
const base = node ?? pickGoosedUrl();
const secret = process.env.GOOSE_SERVER__SECRET_KEY ??
const secret =
process.env.GOOSE_SERVER__SECRET_KEY ??
process.env.TKMIND_SERVER__SECRET_KEY ??
'local-dev-secret';
const url = `${base}${path}`;
const init = {
method,
headers: {
'Content-Type': 'application/json',
'X-Secret-Key': secret,
},
agent: tlsAgent,
};
if (body !== undefined) init.body = JSON.stringify(body);
const url = new URL(`${base}${path}`);
const payload = body !== undefined ? JSON.stringify(body) : null;
const res = await fetch(url, init);
let json = null;
const text = await res.text();
try { json = JSON.parse(text); } catch { json = { raw: text }; }
return new Promise((resolve, reject) => {
const options = {
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname + url.search,
method,
headers: {
'Content-Type': 'application/json',
'X-Secret-Key': secret,
...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
},
agent: tlsAgent,
};
return { ok: res.ok, status: res.status, body: json };
const req = https.request(options, (res) => {
let text = '';
res.setEncoding('utf8');
res.on('data', (chunk) => { text += chunk; });
res.on('end', () => {
let json = null;
try { json = JSON.parse(text); } catch { json = { raw: text }; }
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, body: json });
});
res.on('error', reject);
});
req.on('error', reject);
if (payload) req.write(payload);
req.end();
});
}
+1 -1
View File
@@ -46,7 +46,7 @@ export async function ensureSkillSchema(pool) {
CREATE TABLE IF NOT EXISTS adm_skill_invocations (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
skill_id INT UNSIGNED NOT NULL,
caller_id INT UNSIGNED NULL,
caller_id VARCHAR(64) NULL,
caller_type VARCHAR(32) NOT NULL DEFAULT 'unknown',
input_snapshot JSON NULL,
status ENUM('pending','running','success','failed','blocked_by_flag')