autocrew 0.3.10 → 0.4.1

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.
@@ -0,0 +1,78 @@
1
+ import type { CommandDef } from "./index.js";
2
+ import { getOption } from "./index.js";
3
+
4
+ export const cmd: CommandDef = {
5
+ name: "render",
6
+ description: "Render a video timeline (TTS + screenshots + Jianying export)",
7
+ usage: "autocrew render <project-slug> [--voice <voiceId>] [--ratio <9:16|16:9>]",
8
+ action: async (args, runner) => {
9
+ const slug = args[0];
10
+ if (!slug) {
11
+ console.error("Usage: autocrew render <project-slug> [--voice <voiceId>] [--ratio <9:16|16:9>]");
12
+ process.exitCode = 1;
13
+ return;
14
+ }
15
+
16
+ const voice = getOption(args, "--voice") || "default";
17
+ const ratio = getOption(args, "--ratio") || "9:16";
18
+
19
+ // Find project and its timeline
20
+ try {
21
+ const { findProject } = await import("../../storage/pipeline-store.js");
22
+ const path = await import("node:path");
23
+ const fs = await import("node:fs/promises");
24
+
25
+ const found = await findProject(slug);
26
+ if (!found) {
27
+ console.error(`Project not found: "${slug}"`);
28
+ process.exitCode = 1;
29
+ return;
30
+ }
31
+
32
+ const timelinePath = path.join(found.dir, "timeline.json");
33
+ let timeline;
34
+ try {
35
+ const raw = await fs.readFile(timelinePath, "utf-8");
36
+ timeline = JSON.parse(raw);
37
+ } catch {
38
+ console.error(`No timeline.json found in project "${slug}".`);
39
+ console.error("Generate a timeline first with autocrew_timeline action='generate'.");
40
+ process.exitCode = 1;
41
+ return;
42
+ }
43
+
44
+ // Try to load studio
45
+ let renderTimeline: any;
46
+ let loadConfig: any;
47
+ try {
48
+ const studio = await import("autocrew-studio");
49
+ renderTimeline = studio.renderTimeline;
50
+ loadConfig = studio.loadConfig;
51
+ } catch {
52
+ console.error("autocrew-studio is not installed. Install it with:");
53
+ console.error(" npm install autocrew-studio");
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+
58
+ const config = loadConfig();
59
+ const outputDir = path.join(found.dir, "render");
60
+
61
+ console.log(`Rendering timeline for "${slug}" (${ratio}, voice: ${voice})...`);
62
+
63
+ const result = await renderTimeline({
64
+ timeline,
65
+ outputDir,
66
+ tts: config.tts,
67
+ screenshot: config.screenshot,
68
+ exporter: config.exporter,
69
+ voice: { voiceId: voice },
70
+ });
71
+
72
+ console.log(`Render complete: ${result.path} (${result.format})`);
73
+ } catch (err: any) {
74
+ console.error(`Render failed: ${err.message || err}`);
75
+ process.exitCode = 1;
76
+ }
77
+ },
78
+ };
package/src/e2e.test.ts CHANGED
@@ -198,16 +198,13 @@ describe("E2E: Draft Action (content generation)", () => {
198
198
  // Should return creator context
199
199
  expect(result.creatorContext).toBeDefined();
200
200
 
201
- // Should return writing instructions with Operating System principles
201
+ // Should return writing instructions with thinking-oriented prompts
202
202
  const instructions = result.writingInstructions as string;
203
- expect(instructions).toContain("EMPATHY FIRST");
204
- expect(instructions).toContain("THEIR WORDS, NOT YOURS");
205
- expect(instructions).toContain("SHOW THE MOVIE");
206
- expect(instructions).toContain("TENSION IS OXYGEN");
207
- expect(instructions).toContain("THE CREATOR IS THE PROOF");
208
- expect(instructions).toContain("TWO-PHASE CREATION");
209
- expect(instructions).toContain("PHASE A");
210
- expect(instructions).toContain("PHASE B");
203
+ expect(instructions).toContain("Writing Instructions");
204
+ expect(instructions).toContain("WHO is reading this");
205
+ expect(instructions).toContain("ONE opinion");
206
+ expect(instructions).toContain("skeleton FIRST");
207
+ expect(instructions).toContain("autocrew_content");
211
208
 
212
209
  // Should return next action guidance
213
210
  expect((result.nextAction as any).tool).toBe("autocrew_content");
@@ -12,6 +12,17 @@ const TEMPLATES: WorkflowDefinition[] = [
12
12
  id: "xiaohongshu_full",
13
13
  name: "小红书一键发布",
14
14
  description: "从选题调研到发布的完整小红书内容流水线。包含 AI 写作、去 AI 痕迹、敏感词审查、封面设计等全流程。",
15
+ restatement: {
16
+ intervalSteps: 3,
17
+ context: [
18
+ "## Restatement — 质量提醒",
19
+ "- 标题必须 ≤20 中文字符",
20
+ "- 风格必须符合 STYLE.md",
21
+ "- 每段必须制造或解决张力,不能只是罗列信息",
22
+ "- 保持创作者第一人称视角",
23
+ "- 不要使用 AI 套话:值得一提的是、综上所述、首先其次最后",
24
+ ].join("\n"),
25
+ },
15
26
  steps: [
16
27
  {
17
28
  id: "research",
@@ -70,6 +81,15 @@ const TEMPLATES: WorkflowDefinition[] = [
70
81
  id: "quick_publish",
71
82
  name: "快速发布",
72
83
  description: "已有内容的快速发布流程。去 AI 痕迹 → 审查 → 平台适配 → 封面 → 发布。",
84
+ restatement: {
85
+ intervalSteps: 3,
86
+ context: [
87
+ "## Restatement — 质量提醒",
88
+ "- 风格必须符合 STYLE.md",
89
+ "- 不要使用 AI 套话",
90
+ "- 发布前确保封面和内容匹配",
91
+ ].join("\n"),
92
+ },
73
93
  steps: [
74
94
  {
75
95
  id: "humanize",
@@ -7,6 +7,7 @@
7
7
  * - Audit trail for debugging
8
8
  */
9
9
  import path from "node:path";
10
+ import type { SessionLogger } from "./logger.js";
10
11
 
11
12
  // --- Types ---
12
13
 
@@ -51,6 +52,8 @@ export interface ToolContext {
51
52
  workspace: WorkspaceState;
52
53
  /** Audit log for this session */
53
54
  audit: AuditEntry[];
55
+ /** Persistent session logger (writes to ~/.autocrew/logs/) */
56
+ logger?: SessionLogger;
54
57
  }
55
58
 
56
59
  // --- Factory ---
@@ -70,13 +73,14 @@ function resolveDataDir(config?: PluginConfig): string {
70
73
  /**
71
74
  * Create a new ToolContext. Called once per plugin registration or MCP session.
72
75
  */
73
- export function createContext(config?: PluginConfig): ToolContext {
76
+ export function createContext(config?: PluginConfig, logger?: SessionLogger): ToolContext {
74
77
  const ctx: ToolContext = {
75
78
  sessionId: generateSessionId(),
76
79
  dataDir: resolveDataDir(config),
77
80
  config: config || {},
78
81
  workspace: {},
79
82
  audit: [],
83
+ logger,
80
84
  };
81
85
  _activeContext = ctx;
82
86
  return ctx;
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import { SessionLogger } from "./logger.js";
6
+
7
+ describe("SessionLogger", () => {
8
+ let tmpDir: string;
9
+ let logger: SessionLogger;
10
+
11
+ beforeEach(async () => {
12
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "autocrew-log-"));
13
+ logger = new SessionLogger(tmpDir, "test-session-001");
14
+ });
15
+
16
+ afterEach(async () => {
17
+ await fs.rm(tmpDir, { recursive: true, force: true });
18
+ });
19
+
20
+ it("writes audit entries as JSONL", async () => {
21
+ await logger.audit({
22
+ tool: "autocrew_content",
23
+ action: "save",
24
+ timestamp: "2026-04-09T10:00:00Z",
25
+ durationMs: 150,
26
+ ok: true,
27
+ });
28
+
29
+ const logPath = path.join(tmpDir, "logs", "test-session-001", "audit.jsonl");
30
+ const raw = await fs.readFile(logPath, "utf-8");
31
+ const lines = raw.trim().split("\n");
32
+ expect(lines).toHaveLength(1);
33
+ expect(JSON.parse(lines[0]).tool).toBe("autocrew_content");
34
+ });
35
+
36
+ it("writes events as JSONL", async () => {
37
+ await logger.event({
38
+ type: "content:created",
39
+ timestamp: "2026-04-09T10:00:00Z",
40
+ data: { contentId: "abc" },
41
+ });
42
+
43
+ const logPath = path.join(tmpDir, "logs", "test-session-001", "events.jsonl");
44
+ const raw = await fs.readFile(logPath, "utf-8");
45
+ const lines = raw.trim().split("\n");
46
+ expect(lines).toHaveLength(1);
47
+ expect(JSON.parse(lines[0]).type).toBe("content:created");
48
+ });
49
+
50
+ it("writes tool IO as JSONL", async () => {
51
+ await logger.toolIO({
52
+ tool: "autocrew_content",
53
+ action: "draft",
54
+ input: { topic_title: "AI编程", platform: "xhs" },
55
+ output: { ok: true, writingInstructions: "..." },
56
+ durationMs: 200,
57
+ timestamp: "2026-04-09T10:00:00Z",
58
+ });
59
+
60
+ const logPath = path.join(tmpDir, "logs", "test-session-001", "tool-io.jsonl");
61
+ const raw = await fs.readFile(logPath, "utf-8");
62
+ const entry = JSON.parse(raw.trim());
63
+ expect(entry.tool).toBe("autocrew_content");
64
+ expect(entry.input.topic_title).toBe("AI编程");
65
+ expect(entry.output.ok).toBe(true);
66
+ });
67
+
68
+ it("appends multiple entries without overwriting", async () => {
69
+ await logger.audit({ tool: "a", timestamp: "t1", durationMs: 1, ok: true });
70
+ await logger.audit({ tool: "b", timestamp: "t2", durationMs: 2, ok: false, error: "boom" });
71
+
72
+ const logPath = path.join(tmpDir, "logs", "test-session-001", "audit.jsonl");
73
+ const lines = (await fs.readFile(logPath, "utf-8")).trim().split("\n");
74
+ expect(lines).toHaveLength(2);
75
+ });
76
+ });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * SessionLogger — persistent JSONL logging for audit entries, events, and tool IO.
3
+ *
4
+ * Writes to {dataDir}/logs/{sessionId}/ with separate files:
5
+ * - audit.jsonl — tool execution audit trail
6
+ * - events.jsonl — workflow events
7
+ * - tool-io.jsonl — full tool input/output for debugging
8
+ */
9
+ import fs from "node:fs/promises";
10
+ import path from "node:path";
11
+ import type { AuditEntry } from "./context.js";
12
+ import type { AutoCrewEvent } from "./events.js";
13
+
14
+ export interface ToolIOEntry {
15
+ tool: string;
16
+ action?: string;
17
+ input: Record<string, unknown>;
18
+ output: Record<string, unknown>;
19
+ durationMs: number;
20
+ timestamp: string;
21
+ }
22
+
23
+ export class SessionLogger {
24
+ private sessionDir: string;
25
+ private initialized = false;
26
+
27
+ constructor(dataDir: string, sessionId: string) {
28
+ this.sessionDir = path.join(dataDir, "logs", sessionId);
29
+ }
30
+
31
+ private async ensureDir(): Promise<void> {
32
+ if (!this.initialized) {
33
+ await fs.mkdir(this.sessionDir, { recursive: true });
34
+ this.initialized = true;
35
+ }
36
+ }
37
+
38
+ private async append(filename: string, data: unknown): Promise<void> {
39
+ await this.ensureDir();
40
+ const line = JSON.stringify(data) + "\n";
41
+ await fs.appendFile(path.join(this.sessionDir, filename), line, "utf-8");
42
+ }
43
+
44
+ async audit(entry: AuditEntry): Promise<void> {
45
+ await this.append("audit.jsonl", entry);
46
+ }
47
+
48
+ async event(event: AutoCrewEvent): Promise<void> {
49
+ await this.append("events.jsonl", event);
50
+ }
51
+
52
+ async toolIO(entry: ToolIOEntry): Promise<void> {
53
+ await this.append("tool-io.jsonl", entry);
54
+ }
55
+ }
56
+
57
+ /** List all session log directories, newest first */
58
+ export async function listLogSessions(dataDir: string): Promise<string[]> {
59
+ const logsDir = path.join(dataDir, "logs");
60
+ try {
61
+ const entries = await fs.readdir(logsDir);
62
+ return entries.sort().reverse();
63
+ } catch {
64
+ return [];
65
+ }
66
+ }
67
+
68
+ /** Delete session logs older than N days */
69
+ export async function cleanOldLogs(dataDir: string, maxAgeDays: number): Promise<number> {
70
+ const logsDir = path.join(dataDir, "logs");
71
+ const cutoff = Date.now() - maxAgeDays * 86400000;
72
+ let deleted = 0;
73
+ try {
74
+ const entries = await fs.readdir(logsDir);
75
+ for (const entry of entries) {
76
+ const stat = await fs.stat(path.join(logsDir, entry));
77
+ if (stat.mtimeMs < cutoff) {
78
+ await fs.rm(path.join(logsDir, entry), { recursive: true });
79
+ deleted++;
80
+ }
81
+ }
82
+ } catch { /* logs dir may not exist */ }
83
+ return deleted;
84
+ }
@@ -172,6 +172,39 @@ const auditMiddleware: Middleware = async (ctx, toolName, params, next) => {
172
172
  return result;
173
173
  };
174
174
 
175
+ /** Persist tool IO to disk via SessionLogger */
176
+ const persistentLogMiddleware: Middleware = async (ctx, toolName, params, next) => {
177
+ const start = Date.now();
178
+ const { _dataDir, _gatewayUrl, _geminiApiKey, _geminiModel, ...loggedInput } = params;
179
+ let result: ToolResult;
180
+ try {
181
+ result = await next();
182
+ } catch (err) {
183
+ if (ctx.logger) {
184
+ await ctx.logger.toolIO({
185
+ tool: toolName,
186
+ action: loggedInput.action as string | undefined,
187
+ input: loggedInput,
188
+ output: { ok: false, error: err instanceof Error ? err.message : String(err) },
189
+ durationMs: Date.now() - start,
190
+ timestamp: new Date().toISOString(),
191
+ });
192
+ }
193
+ throw err;
194
+ }
195
+ if (ctx.logger) {
196
+ await ctx.logger.toolIO({
197
+ tool: toolName,
198
+ action: loggedInput.action as string | undefined,
199
+ input: loggedInput,
200
+ output: result,
201
+ durationMs: Date.now() - start,
202
+ timestamp: new Date().toISOString(),
203
+ });
204
+ }
205
+ return result;
206
+ };
207
+
175
208
  /** Update workspace state based on tool results */
176
209
  const workspaceTrackingMiddleware: Middleware = async (ctx, toolName, params, next) => {
177
210
  const result = await next();
@@ -206,6 +239,7 @@ export class ToolRunner {
206
239
  onboardingGateMiddleware,
207
240
  errorBoundaryMiddleware,
208
241
  auditMiddleware,
242
+ persistentLogMiddleware,
209
243
  workspaceTrackingMiddleware,
210
244
  ...(options.middleware || []),
211
245
  ];
@@ -452,4 +452,73 @@ describe("WorkflowEngine", () => {
452
452
  it("throws when starting a nonexistent workflow", async () => {
453
453
  await expect(engine.start("nonexistent")).rejects.toThrow("not found");
454
454
  });
455
+
456
+ describe("restatement", () => {
457
+ it("injects restatement context every N steps", async () => {
458
+ runner.register({
459
+ name: "echo_tool",
460
+ label: "Echo",
461
+ description: "always succeeds",
462
+ parameters: {},
463
+ execute: vi.fn(async () => ({ ok: true, value: "done" })),
464
+ });
465
+
466
+ const def: WorkflowDefinition = {
467
+ id: "restate-test",
468
+ name: "Restatement Test",
469
+ description: "Tests restatement injection",
470
+ steps: Array.from({ length: 6 }, (_, i) => ({
471
+ id: `step-${i}`,
472
+ name: `Step ${i}`,
473
+ tool: "echo_tool",
474
+ params: {},
475
+ })),
476
+ restatement: {
477
+ intervalSteps: 3,
478
+ context: "IMPORTANT: Follow quality rules.",
479
+ },
480
+ };
481
+
482
+ engine.registerDefinition(def);
483
+ const instance = await engine.create("restate-test");
484
+ const result = await engine.start(instance.id);
485
+
486
+ expect(result.status).toBe("completed");
487
+ // After step index 3 (steps 0,1,2 done), restatement should fire
488
+ expect(result.stepResults["_restatement_after_step-2"]).toBeDefined();
489
+ expect((result.stepResults["_restatement_after_step-2"] as any).context).toBe("IMPORTANT: Follow quality rules.");
490
+ // After step index 6 (steps 3,4,5 done), another restatement
491
+ expect(result.stepResults["_restatement_after_step-5"]).toBeDefined();
492
+ });
493
+
494
+ it("does not inject restatement when not configured", async () => {
495
+ runner.register({
496
+ name: "echo_tool2",
497
+ label: "Echo2",
498
+ description: "always succeeds",
499
+ parameters: {},
500
+ execute: vi.fn(async () => ({ ok: true, value: "done" })),
501
+ });
502
+
503
+ const def: WorkflowDefinition = {
504
+ id: "no-restate",
505
+ name: "No Restatement",
506
+ description: "No restatement configured",
507
+ steps: Array.from({ length: 4 }, (_, i) => ({
508
+ id: `s-${i}`,
509
+ name: `Step ${i}`,
510
+ tool: "echo_tool2",
511
+ params: {},
512
+ })),
513
+ };
514
+
515
+ engine.registerDefinition(def);
516
+ const instance = await engine.create("no-restate");
517
+ const result = await engine.start(instance.id);
518
+
519
+ expect(result.status).toBe("completed");
520
+ const restateKeys = Object.keys(result.stepResults).filter(k => k.startsWith("_restatement_"));
521
+ expect(restateKeys).toHaveLength(0);
522
+ });
523
+ });
455
524
  });
@@ -35,6 +35,13 @@ export interface WorkflowDefinition {
35
35
  name: string;
36
36
  description: string;
37
37
  steps: WorkflowStep[];
38
+ /** Optional: inject restatement context every N steps to combat attention decay */
39
+ restatement?: {
40
+ /** Inject restatement every N steps */
41
+ intervalSteps: number;
42
+ /** The context string to restate (key rules, current goal, quality constraints) */
43
+ context: string;
44
+ };
38
45
  }
39
46
 
40
47
  export type WorkflowStatus = "pending" | "running" | "paused" | "completed" | "failed" | "cancelled";
@@ -350,6 +357,19 @@ export class WorkflowEngine {
350
357
 
351
358
  // Advance
352
359
  instance.currentStepIndex++;
360
+
361
+ // Restatement injection — combat attention decay in long workflows
362
+ if (def.restatement && instance.currentStepIndex > 0 && instance.currentStepIndex % def.restatement.intervalSteps === 0) {
363
+ const restateKey = `_restatement_after_${step.id}`;
364
+ instance.stepResults[restateKey] = {
365
+ type: "restatement",
366
+ context: def.restatement.context,
367
+ afterStep: step.id,
368
+ stepIndex: instance.currentStepIndex,
369
+ timestamp: new Date().toISOString(),
370
+ };
371
+ }
372
+
353
373
  await this.persistInstance(instance);
354
374
  }
355
375
 
@@ -149,39 +149,21 @@ Clock Theory: Bang moments at 12:00(hook), 3:00(escalation), 6:00(payload), 9:00
149
149
  methodology,
150
150
  wikiContext: wikiContext || "(no wiki knowledge available yet)",
151
151
  writingInstructions: `
152
- You are now writing a content draft for Chinese social media. Follow these instructions EXACTLY.
152
+ ## Writing Instructions
153
153
 
154
- ## THE OPERATING SYSTEM 5 Principles (override everything else)
154
+ Think through these questions before writing:
155
155
 
156
- 1. EMPATHY FIRST You are sitting across from ONE person (the audiencePersona). They are scrolling, half-distracted. Every sentence must earn their next 3 seconds. If a sentence triggers no curiosity, recognition, surprise, or relief — delete it.
156
+ 1. WHO is reading this? (Reference the audiencePersona above)
157
+ 2. WHAT is the ONE opinion this content argues? (Not a topic — an opinion)
158
+ 3. WHY would a reader stop scrolling for this? (The hook)
159
+ 4. WHAT evidence makes this believable? (Creator's own experience > external data)
157
160
 
158
- 2. THEIR WORDS, NOT YOURS — Every word must pass: would the reader say this to a friend over coffee? If no, replace it. No jargon, no abstractions, no "smart" words the reader wouldn't use.
161
+ Then:
162
+ - Build a skeleton FIRST (thesis → evidence → twist → action). Present it for confirmation.
163
+ - Write the full draft only after skeleton is confirmed.
164
+ - Every paragraph must either open a question or close one. No "information-only" paragraphs.
159
165
 
160
- 3. SHOW THE MOVIE Abstractions are invisible. Stories are visible. Every claim needs a scene: a face, a number, a moment. "She built a product in 3 weeks, alone" > "AI improves efficiency."
161
-
162
- 4. TENSION IS OXYGEN — Every paragraph must either OPEN a question or CLOSE one. No paragraph should just "sit there" as information. When energy drops, inject a question, contradiction, or surprise.
163
-
164
- 5. THE CREATOR IS THE PROOF — The creator's own experience is the strongest evidence. Lead with "I did X" before "Company Y did Z." Vulnerability > authority.
165
-
166
- ## TWO-PHASE CREATION
167
-
168
- PHASE A — Build the skeleton FIRST (do not write prose yet):
169
- - A1: Core thesis in ONE sentence (an opinion, not a topic)
170
- - A2: Argument structure (thesis → evidence → twist → action)
171
- - A3: Clock Theory — plan 4 bang moments (12:00 hook, 3:00 escalation, 6:00 payload, 9:00 climax)
172
- - A4: HKRR — choose dominant dimension
173
- - A5: Place 2-3 micro-retention techniques
174
-
175
- Present the skeleton to the user. Wait for confirmation. Then:
176
-
177
- PHASE B — Write the full draft based on the skeleton.
178
- - Fear short, not long. Every case study deserves full detail.
179
- - After each clock section, simulate the reader's reaction.
180
- - Ground every factual claim in real sources.
181
-
182
- ## AFTER WRITING
183
-
184
- Call autocrew_content action="save" with the full body text, title, platform, and hypothesis.
166
+ Save via autocrew_content action="save" with title, body, platform, and hypothesis.
185
167
  Do NOT use the Write tool to create draft.md directly.
186
168
  `,
187
169
  nextAction: {
@@ -14,50 +14,9 @@
14
14
  10. After completing a task, suggest one concrete next step.
15
15
  11. When user gives feedback on content, capture it via the memory-distill skill.
16
16
 
17
- ## Onboarding Protocol (MANDATORY — cannot be skipped)
17
+ ## Onboarding Protocol
18
18
 
19
- This is the FIRST thing that happens for any new user. No exceptions.
20
-
21
- ### Gate Check
22
- 1. Call `autocrew_pro_status` at session start.
23
- 2. If ANY of these are true → BLOCK all other actions and run onboarding:
24
- - `profileExists: false`
25
- - `missingInfo` array is non-empty
26
- - `styleCalibrated: false`
27
-
28
- ### Onboarding Flow (3 phases, must complete all)
29
-
30
- **Phase 1: 初始化 + 基础信息(2-3 轮对话)**
31
- 1. Call `autocrew_init` to create data directory
32
- 2. Read host MEMORY.md if it exists (extract known info)
33
- 3. Ask for missing fields ONLY:
34
- - 行业/领域(必填)
35
- - 目标平台(必填,可多选:小红书/抖音/公众号/视频号)
36
- - 目标受众(必填:年龄段、职业、痛点)
37
- - 变现模式(选填:广告/带货/知识付费/引流)
38
- 4. Save to creator-profile.json
39
-
40
- **Phase 2: 风格校准(3-5 轮对话)**
41
- 1. 询问用户是否有参考账号或已有内容样本
42
- 2. 如果有 → 分析样本,提取风格特征
43
- 3. 如果没有 → 通过 A/B 对比问题确定风格偏好:
44
- - 正式 vs 口语
45
- - 专业术语 vs 大白话
46
- - 长文深度 vs 短文快节奏
47
- - 情感共鸣 vs 干货实用
48
- 4. 生成 STYLE.md 写作人格文件
49
- 5. 更新 creator-profile.json 的 `styleCalibrated: true`
50
-
51
- **Phase 3: 确认 + 过渡**
52
- 1. 展示生成的风格档案摘要
53
- 2. 告诉用户"设置完成,现在可以开始创作了"
54
- 3. 然后继续用户的原始请求(如果有的话)
55
-
56
- ### 重要:不要把 onboarding 做成审讯
57
- - 语气轻松友好,像朋友聊天
58
- - 每次最多问 2-3 个问题
59
- - 已知信息(从 MEMORY.md 读到的)直接确认,不重复问
60
- - 风格校准用选择题,不要让用户写长文
19
+ Onboarding is enforced by the tool system. If a tool returns `error: "onboarding_required"`, `error: "profile_incomplete"`, or `error: "style_not_calibrated"`, follow the returned instructions to complete setup before proceeding. Keep the tone conversational — like chatting with a friend, not an interrogation.
61
20
 
62
21
  ## Memory Protocol
63
22
 
@@ -74,26 +33,13 @@ This is the FIRST thing that happens for any new user. No exceptions.
74
33
 
75
34
  ## Skill Routing
76
35
 
77
- | User intent | Skill to load |
78
- |-------------|---------------|
36
+ | User intent | Skill |
37
+ |-------------|-------|
79
38
  | First use / profile incomplete | onboarding |
80
- | /setup / "设置" / "风格校准" / "品牌校准" / "calibrate" | setup |
81
- | "帮我找选题" / "调研" / "这周写什么" / "内容规划" | spawn-planner or research (must also load title-craft for title methodology) |
82
- | "帮我想" / "想选题" / seed idea | topic-ideas (must also load title-craft for title methodology) |
83
- | "受众分析" / "用户画像" / "audience" | audience-profiler |
84
- | "二创" / "换个角度写" / "remix" | remix-content |
85
- | "写这个" / "帮我写" / "写一篇" | spawn-writer |
86
- | "批量写" / "都写了" / "写N篇" | spawn-batch-writer |
87
- | "改写" / "适配" / "发到XX平台" | platform-rewrite |
88
- | "去AI味" / "润色" | humanizer-zh |
89
- | "审核" / "检查" / "敏感词" | content-review |
90
- | "封面" / "生成封面" / "做个封面" | cover-generator |
91
- | "发布前检查" | pre-publish |
92
- | "发布" / "发到小红书" | publish-content |
93
- | "自动化" / "定时" / "pipeline" | manage-pipeline |
39
+ | "设置" / "风格校准" / "calibrate" | setup |
40
+ | "帮我写" / "写一篇" / "写这个" | spawn-writer (loads write-script + title-craft) |
41
+ | "批量写" / "都写了" | spawn-batch-writer |
42
+ | "帮我找选题" / "调研" / "内容规划" | spawn-planner or research (loads title-craft) |
94
43
  | User gives feedback on content | memory-distill |
95
- | "状态" / "进度" | autocrew_status tool |
96
- | "对标" / "监控" / competitor URL | [Pro] competitor-monitor |
97
- | Video/note URL + "分析/拆解" | [Pro] video-analysis |
98
- | Video/note URL (no analysis intent) | [Pro] extract-video-script |
99
- | "数据" / "分析报告" | [Pro] analytics-report |
44
+
45
+ For other intents (封面, 发布, 审核, 去AI味, 改写, 状态, 对标, 数据), use the corresponding autocrew_* tool directly — the tool names are self-explanatory.