From cb61e989799806cc9fb505d97aa0ae05ede50df9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Tue, 28 Apr 2026 04:00:38 +0000 Subject: [PATCH] =?UTF-8?q?refactor(sense-generator):=20full=20DIP=20?= =?UTF-8?q?=E2=80=94=20all=20deps=20injected=20via=20build=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every role is self-contained (types.ts, prompt.ts, index.ts). No shared.ts, no cross-role imports. All dependencies injected: index.ts — wiring (resolve env, call buildSenseGenerator) build.ts — buildSenseGenerator(deps) → WorkflowDefinition moderator.ts — pure routing, composes meta from role types roles/planner/ — buildPlannerRole(deps), self-contained roles/coder/ — buildCoderRole(deps), self-contained roles/tester/ — buildTesterRole(deps), self-contained Workflow is now reusable: buildSenseGenerator() can be called with any provider/paths, not hardcoded to this machine. 小橘 🍊(NEKO Team) --- workflows/sense-generator/build.ts | 41 +++++++++ workflows/sense-generator/index.ts | 91 +++++++++++++++---- workflows/sense-generator/moderator.ts | 10 +- .../sense-generator/roles/coder/index.ts | 23 +++-- .../sense-generator/roles/coder/types.ts | 7 ++ .../sense-generator/roles/planner/index.ts | 24 +++-- .../sense-generator/roles/planner/types.ts | 7 ++ workflows/sense-generator/roles/shared.ts | 63 ------------- .../sense-generator/roles/tester/index.ts | 20 ++-- .../sense-generator/roles/tester/types.ts | 7 ++ workflows/sense-generator/roles/types.ts | 19 ---- 11 files changed, 185 insertions(+), 127 deletions(-) create mode 100644 workflows/sense-generator/build.ts create mode 100644 workflows/sense-generator/roles/coder/types.ts create mode 100644 workflows/sense-generator/roles/planner/types.ts delete mode 100644 workflows/sense-generator/roles/shared.ts create mode 100644 workflows/sense-generator/roles/tester/types.ts delete mode 100644 workflows/sense-generator/roles/types.ts diff --git a/workflows/sense-generator/build.ts b/workflows/sense-generator/build.ts new file mode 100644 index 0000000..d0a6a62 --- /dev/null +++ b/workflows/sense-generator/build.ts @@ -0,0 +1,41 @@ +import type { WorkflowDefinition } from "@uncaged/nerve-core"; +import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; +import { buildPlannerRole } from "./roles/planner/index.js"; +import { buildCoderRole } from "./roles/coder/index.js"; +import { buildTesterRole } from "./roles/tester/index.js"; +import { moderator } from "./moderator.js"; +import type { SenseMeta } from "./moderator.js"; + +export type BuildSenseGeneratorDeps = { + provider: LlmProvider; + nerveRoot: string; + sensesDir: string; + senseExamples: string; + nerveYaml: string; +}; + +export function buildSenseGenerator(deps: BuildSenseGeneratorDeps): WorkflowDefinition { + return { + name: "sense-generator", + roles: { + planner: buildPlannerRole({ + provider: deps.provider, + cwd: deps.nerveRoot, + senseExamples: deps.senseExamples, + nerveYaml: deps.nerveYaml, + }), + coder: buildCoderRole({ + provider: deps.provider, + cwd: deps.nerveRoot, + sensesDir: deps.sensesDir, + nerveRoot: deps.nerveRoot, + }), + tester: buildTesterRole({ + provider: deps.provider, + sensesDir: deps.sensesDir, + nerveRoot: deps.nerveRoot, + }), + }, + moderator, + }; +} diff --git a/workflows/sense-generator/index.ts b/workflows/sense-generator/index.ts index 594c936..49e0842 100644 --- a/workflows/sense-generator/index.ts +++ b/workflows/sense-generator/index.ts @@ -1,24 +1,77 @@ -import type { WorkflowDefinition } from "@uncaged/nerve-core"; -import { buildPlannerRole } from "./roles/planner/index.js"; -import { buildCoderRole } from "./roles/coder/index.js"; -import { buildTesterRole } from "./roles/tester/index.js"; -import { moderator } from "./moderator.js"; -import { resolveDashScopeProvider } from "./roles/shared.js"; -import type { SenseMeta } from "./roles/types.js"; +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { spawnSafe } from "@uncaged/nerve-workflow-utils"; +import { buildSenseGenerator } from "./build.js"; -const provider = await resolveDashScopeProvider(); -if (provider === null) { - throw new Error("Cannot build workflow: set DASHSCOPE_API_KEY and DASHSCOPE_BASE_URL"); +// --- Environment --- + +const HOME = process.env.HOME ?? "/home/azureuser"; +const NERVE_ROOT = join(HOME, ".uncaged-nerve"); +const SENSES_DIR = join(NERVE_ROOT, "senses"); + +// --- Resolve provider --- + +async function cfgGet(key: string): Promise { + const result = await spawnSafe("cfg", ["get", key], { + cwd: NERVE_ROOT, + env: null, + timeoutMs: 10_000, + }); + if (!result.ok) return null; + return result.value.stdout.trim() || null; } -const workflow: WorkflowDefinition = { - name: "sense-generator", - roles: { - planner: buildPlannerRole(provider), - coder: buildCoderRole(provider), - tester: buildTesterRole(provider), - }, - moderator, -}; +const apiKey = process.env.DASHSCOPE_API_KEY ?? (await cfgGet("DASHSCOPE_API_KEY")); +const baseUrl = process.env.DASHSCOPE_BASE_URL ?? (await cfgGet("DASHSCOPE_BASE_URL")); +const model = process.env.DASHSCOPE_MODEL ?? (await cfgGet("DASHSCOPE_MODEL")) ?? "qwen-plus"; +if (!apiKey || !baseUrl) { + throw new Error("Set DASHSCOPE_API_KEY and DASHSCOPE_BASE_URL"); +} + +// --- Build context --- + +function getNerveYaml(): string { + try { + return readFileSync(join(NERVE_ROOT, "nerve.yaml"), "utf-8"); + } catch { + return "# nerve.yaml unavailable"; + } +} + +function buildSenseExamples(): string { + const examples: string[] = []; + for (const name of ["cpu-usage", "linux-system-health"]) { + const dir = join(SENSES_DIR, name); + if (!existsSync(dir)) continue; + const indexFile = existsSync(join(dir, "index.js")) + ? readFileSync(join(dir, "index.js"), "utf-8") + : ""; + const schema = existsSync(join(dir, "schema.ts")) + ? readFileSync(join(dir, "schema.ts"), "utf-8") + : ""; + const migrationDir = join(dir, "migrations"); + let migration = ""; + if (existsSync(join(migrationDir, "0001_init.sql"))) { + migration = readFileSync(join(migrationDir, "0001_init.sql"), "utf-8"); + } + examples.push( + `### Example sense: ${name}\n\n` + + `**index.js:**\n\`\`\`js\n${indexFile}\n\`\`\`\n\n` + + `**schema.ts:**\n\`\`\`ts\n${schema}\n\`\`\`\n\n` + + `**migrations/0001_init.sql:**\n\`\`\`sql\n${migration}\n\`\`\``, + ); + } + return examples.join("\n\n---\n\n"); +} + +// --- Wire up --- + +const workflow = buildSenseGenerator({ + provider: { apiKey, baseUrl, model }, + nerveRoot: NERVE_ROOT, + sensesDir: SENSES_DIR, + senseExamples: buildSenseExamples(), + nerveYaml: getNerveYaml(), +}); export default workflow; diff --git a/workflows/sense-generator/moderator.ts b/workflows/sense-generator/moderator.ts index abc3b3c..65cb8f4 100644 --- a/workflows/sense-generator/moderator.ts +++ b/workflows/sense-generator/moderator.ts @@ -1,6 +1,14 @@ import { END } from "@uncaged/nerve-core"; import type { Moderator } from "@uncaged/nerve-core"; -import type { SenseMeta } from "./roles/types.js"; +import type { PlannerMeta } from "./roles/planner/types.js"; +import type { CoderMeta } from "./roles/coder/types.js"; +import type { TesterMeta } from "./roles/tester/types.js"; + +export type SenseMeta = { + planner: PlannerMeta; + coder: CoderMeta; + tester: TesterMeta; +}; function countRole(steps: { role: string }[], name: string): number { return steps.filter((s) => s.role === name).length; diff --git a/workflows/sense-generator/roles/coder/index.ts b/workflows/sense-generator/roles/coder/index.ts index 1d8598b..90a019b 100644 --- a/workflows/sense-generator/roles/coder/index.ts +++ b/workflows/sense-generator/roles/coder/index.ts @@ -1,15 +1,22 @@ import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; import { createCursorRole } from "@uncaged/nerve-workflow-utils"; -import { NERVE_ROOT, SENSES_DIR } from "../shared.js"; -import { coderMetaSchema } from "../types.js"; -import type { SenseMeta } from "../types.js"; +import { coderMetaSchema } from "./types.js"; +import type { CoderMeta } from "./types.js"; import { coderPrompt } from "./prompt.js"; -export function buildCoderRole(provider: LlmProvider) { - return createCursorRole({ - cwd: NERVE_ROOT, +export type BuildCoderDeps = { + provider: LlmProvider; + cwd: string; + sensesDir: string; + nerveRoot: string; +}; + +export function buildCoderRole(deps: BuildCoderDeps) { + return createCursorRole({ + cwd: deps.cwd, mode: "default", - prompt: async (threadId) => coderPrompt({ threadId, sensesDir: SENSES_DIR, nerveRoot: NERVE_ROOT }), - extract: { provider, schema: coderMetaSchema }, + prompt: async (threadId) => + coderPrompt({ threadId, sensesDir: deps.sensesDir, nerveRoot: deps.nerveRoot }), + extract: { provider: deps.provider, schema: coderMetaSchema }, }); } diff --git a/workflows/sense-generator/roles/coder/types.ts b/workflows/sense-generator/roles/coder/types.ts new file mode 100644 index 0000000..cd2e19d --- /dev/null +++ b/workflows/sense-generator/roles/coder/types.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export type CoderMeta = { filesCreated: boolean }; + +export const coderMetaSchema = z.object({ + filesCreated: z.boolean().describe("true if the sense files were created"), +}); diff --git a/workflows/sense-generator/roles/planner/index.ts b/workflows/sense-generator/roles/planner/index.ts index a929894..1376b74 100644 --- a/workflows/sense-generator/roles/planner/index.ts +++ b/workflows/sense-generator/roles/planner/index.ts @@ -1,18 +1,22 @@ import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; import { createCursorRole } from "@uncaged/nerve-workflow-utils"; -import { buildSenseExamples, getNerveYaml, NERVE_ROOT } from "../shared.js"; -import { plannerMetaSchema } from "../types.js"; -import type { SenseMeta } from "../types.js"; +import { plannerMetaSchema } from "./types.js"; +import type { PlannerMeta } from "./types.js"; import { plannerPrompt } from "./prompt.js"; -const senseExamples = buildSenseExamples(); -const nerveYaml = getNerveYaml(); +export type BuildPlannerDeps = { + provider: LlmProvider; + cwd: string; + senseExamples: string; + nerveYaml: string; +}; -export function buildPlannerRole(provider: LlmProvider) { - return createCursorRole({ - cwd: NERVE_ROOT, +export function buildPlannerRole(deps: BuildPlannerDeps) { + return createCursorRole({ + cwd: deps.cwd, mode: "ask", - prompt: async (threadId) => plannerPrompt({ threadId, senseExamples, nerveYaml }), - extract: { provider, schema: plannerMetaSchema }, + prompt: async (threadId) => + plannerPrompt({ threadId, senseExamples: deps.senseExamples, nerveYaml: deps.nerveYaml }), + extract: { provider: deps.provider, schema: plannerMetaSchema }, }); } diff --git a/workflows/sense-generator/roles/planner/types.ts b/workflows/sense-generator/roles/planner/types.ts new file mode 100644 index 0000000..d15bfb2 --- /dev/null +++ b/workflows/sense-generator/roles/planner/types.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export type PlannerMeta = { senseName: string }; + +export const plannerMetaSchema = z.object({ + senseName: z.string().describe("kebab-case sense name from the plan"), +}); diff --git a/workflows/sense-generator/roles/shared.ts b/workflows/sense-generator/roles/shared.ts deleted file mode 100644 index 3efa84b..0000000 --- a/workflows/sense-generator/roles/shared.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { spawnSafe } from "@uncaged/nerve-workflow-utils"; -import { existsSync, readFileSync } from "node:fs"; -import { join } from "node:path"; - -export const HOME = process.env.HOME ?? "/home/azureuser"; -export const NERVE_ROOT = join(HOME, ".uncaged-nerve"); -export const SENSES_DIR = join(NERVE_ROOT, "senses"); - -export async function cfgGet(key: string): Promise { - const result = await spawnSafe("cfg", ["get", key], { - cwd: NERVE_ROOT, - env: null, - timeoutMs: 10_000, - }); - if (!result.ok) return null; - return result.value.stdout.trim() || null; -} - -export async function resolveDashScopeProvider(): Promise<{ - baseUrl: string; - apiKey: string; - model: string; -} | null> { - const apiKey = process.env.DASHSCOPE_API_KEY ?? (await cfgGet("DASHSCOPE_API_KEY")); - const baseUrl = process.env.DASHSCOPE_BASE_URL ?? (await cfgGet("DASHSCOPE_BASE_URL")); - const model = process.env.DASHSCOPE_MODEL ?? (await cfgGet("DASHSCOPE_MODEL")) ?? "qwen-plus"; - if (!apiKey || !baseUrl) return null; - return { apiKey, baseUrl, model }; -} - -export function getNerveYaml(): string { - try { - return readFileSync(join(NERVE_ROOT, "nerve.yaml"), "utf-8"); - } catch { - return "# nerve.yaml unavailable"; - } -} - -export function buildSenseExamples(): string { - const examples: string[] = []; - for (const name of ["cpu-usage", "linux-system-health"]) { - const dir = join(SENSES_DIR, name); - if (!existsSync(dir)) continue; - const indexFile = existsSync(join(dir, "index.js")) - ? readFileSync(join(dir, "index.js"), "utf-8") - : ""; - const schema = existsSync(join(dir, "schema.ts")) - ? readFileSync(join(dir, "schema.ts"), "utf-8") - : ""; - const migrationDir = join(dir, "migrations"); - let migration = ""; - if (existsSync(join(migrationDir, "0001_init.sql"))) { - migration = readFileSync(join(migrationDir, "0001_init.sql"), "utf-8"); - } - examples.push( - `### Example sense: ${name}\n\n` + - `**index.js:**\n\`\`\`js\n${indexFile}\n\`\`\`\n\n` + - `**schema.ts:**\n\`\`\`ts\n${schema}\n\`\`\`\n\n` + - `**migrations/0001_init.sql:**\n\`\`\`sql\n${migration}\n\`\`\``, - ); - } - return examples.join("\n\n---\n\n"); -} diff --git a/workflows/sense-generator/roles/tester/index.ts b/workflows/sense-generator/roles/tester/index.ts index c9508ab..2d5e1d7 100644 --- a/workflows/sense-generator/roles/tester/index.ts +++ b/workflows/sense-generator/roles/tester/index.ts @@ -1,13 +1,19 @@ import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; import { createHermesRole } from "@uncaged/nerve-workflow-utils"; -import { NERVE_ROOT, SENSES_DIR } from "../shared.js"; -import { testerMetaSchema } from "../types.js"; -import type { SenseMeta } from "../types.js"; +import { testerMetaSchema } from "./types.js"; +import type { TesterMeta } from "./types.js"; import { testerPrompt } from "./prompt.js"; -export function buildTesterRole(provider: LlmProvider) { - return createHermesRole({ - prompt: async (threadId) => testerPrompt({ threadId, sensesDir: SENSES_DIR, nerveRoot: NERVE_ROOT }), - extract: { provider, schema: testerMetaSchema }, +export type BuildTesterDeps = { + provider: LlmProvider; + sensesDir: string; + nerveRoot: string; +}; + +export function buildTesterRole(deps: BuildTesterDeps) { + return createHermesRole({ + prompt: async (threadId) => + testerPrompt({ threadId, sensesDir: deps.sensesDir, nerveRoot: deps.nerveRoot }), + extract: { provider: deps.provider, schema: testerMetaSchema }, }); } diff --git a/workflows/sense-generator/roles/tester/types.ts b/workflows/sense-generator/roles/tester/types.ts new file mode 100644 index 0000000..187ff21 --- /dev/null +++ b/workflows/sense-generator/roles/tester/types.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export type TesterMeta = { passed: boolean }; + +export const testerMetaSchema = z.object({ + passed: z.boolean().describe("true if all e2e checks passed"), +}); diff --git a/workflows/sense-generator/roles/types.ts b/workflows/sense-generator/roles/types.ts deleted file mode 100644 index bc52875..0000000 --- a/workflows/sense-generator/roles/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { z } from "zod"; - -export type SenseMeta = { - planner: { senseName: string }; - coder: { filesCreated: boolean }; - tester: { passed: boolean }; -}; - -export const plannerMetaSchema = z.object({ - senseName: z.string().describe("kebab-case sense name from the plan"), -}); - -export const coderMetaSchema = z.object({ - filesCreated: z.boolean().describe("true if the sense files were created"), -}); - -export const testerMetaSchema = z.object({ - passed: z.boolean().describe("true if all e2e checks passed"), -});