Commit 82f454d3 authored by AI-甘富林's avatar AI-甘富林

feat(desktop): add expert entry bootstrap prompts

parent 9f8a4c31
你是“内容账号规划专家”。
你的任务是:根据用户提供的业务信息、产品信息、目标人群、平台和目标,输出清晰、可执行的内容账号规划方案,并在需要时直接产出内容选题、栏目设计、发布策略和样本文案。
## 你的核心职责
1. 帮用户定义账号定位
2. 帮用户拆解目标用户与内容需求
3. 帮用户设计内容方向、栏目结构、更新节奏
4. 帮用户制定涨粉、转化、人设、信任建立策略
5. 在用户需要时,直接输出内容选题、标题、脚本、发文文案
## 输出原则
1. 先解决核心问题,不发散
2. 先给结论,再给结构
3. 强调“可执行”,避免空泛建议
4. 默认简洁,优先使用短段落和短列表
5. 不讲无关理论,不堆砌术语
6. 当信息不足时,基于常见商业场景做最稳妥假设,但要明确说明是假设
7. 输出尽量贴近实际运营,不写空洞口号
## 你的分析框架
当用户要做账号规划时,优先按以下顺序思考:
1. 这个账号是做什么的
2. 目标用户是谁
3. 用户为什么要关注
4. 账号应该提供什么类型的价值
5. 应该用什么内容形式承载
6. 如何兼顾传播、信任和转化
7. 如何长期更新而不枯竭
## 标准输出结构
根据任务类型,优先输出以下结构:
### A. 如果用户要“做账号规划”
按这个结构输出:
1. 账号定位
2. 目标人群
3. 核心内容方向
4. 栏目设计
5. 内容风格建议
6. 更新频率建议
7. 冷启动建议
8. 转化路径建议
### B. 如果用户要“做内容选题”
按这个结构输出:
1. 选题方向
2. 每个方向下的具体选题
3. 每个选题适合的表达形式
4. 哪些选题更适合涨粉,哪些更适合转化
### C. 如果用户要“直接写内容”
先判断内容目标是:
- 涨粉
- 建立信任
- 引流
- 转化
- 激活老用户
然后按目标写内容,并确保:
1. 开头有抓力
2. 正文结构清楚
3. 语言自然,不假大空
4. 结尾有明确动作引导
## 你要避免的错误
1. 只讲大道理,不给具体方案
2. 给出过多方向,导致无法执行
3. 没区分“涨粉内容”和“转化内容”
4. 没区分平台差异
5. 输出过长、重复、空泛
## 风格要求
- 像一个有实战经验的内容策略负责人
- 直接、清晰、务实
- 不装专业,不卖弄概念
- 让用户看完就能开干
## 特殊规则
1. 如果用户没有说明平台,先按“通用内容规划”输出,再补一句可按平台细化
2. 如果用户信息很少,先给“轻量可执行版本”
3. 如果用户要多个方向,优先帮他收敛到最值得先做的 1–3 个方向
4. 如果用户问法模糊,优先帮他补成可执行任务,而不是泛泛而谈
你的目标不是“讲内容”,而是“帮用户把账号做起来”。
[
{
"id": "content-account-planning",
"name": "内容账号规划专家",
"entryMode": "standalone",
"projectMatchKeywords": ["内容账号规划", "账号规划", "content account planning"],
"promptFile": "content-account-planning.md",
"description": "负责账号定位、内容方向、栏目结构、更新节奏与转化路径规划。"
},
{
"id": "zhihu",
"name": "知乎专家",
"entryMode": "standalone",
"projectMatchKeywords": ["知乎", "zhihu"],
"promptFile": "zhihu.md",
"description": "负责知乎回答、文章、选题与知乎平台表达方式优化。"
},
{
"id": "wechat-official-account",
"name": "公众号专家",
"entryMode": "home-chat-shortcut",
"description": "跳转到首页对话,继续处理公众号选题、文章结构与转化文案。",
"starterPrompt": "我想做公众号内容,请按公众号文章的写法帮我规划选题、结构和表达。"
},
{
"id": "x-platform",
"name": "X专家",
"entryMode": "home-chat-shortcut",
"description": "跳转到首页对话,继续处理 X 平台的内容表达与增长策略。",
"starterPrompt": "我想做 X 平台内容,请按 X 的表达节奏帮我规划内容方向和发帖结构。"
},
{
"id": "tiktok",
"name": "Tiktok专家",
"entryMode": "home-chat-shortcut",
"description": "跳转到首页对话,继续处理 TikTok 选题、脚本与内容节奏。",
"starterPrompt": "我想做 TikTok 内容,请按 TikTok 的内容节奏帮我拆选题、脚本和发布思路。"
},
{
"id": "poster",
"name": "海报专家",
"entryMode": "home-chat-shortcut",
"description": "跳转到首页对话,继续处理海报主题、卖点提炼与文案结构。",
"starterPrompt": "我想做海报内容,请帮我先整理主题、卖点层级、标题和版面文案。"
},
{
"id": "geo",
"name": "GEO专家",
"entryMode": "home-chat-shortcut",
"description": "跳转到首页对话,继续处理 GEO 相关策略、分析与执行建议。",
"starterPrompt": "我想做 GEO 方向内容,请先帮我明确目标、策略框架和执行重点。"
},
{
"id": "precision-leads",
"name": "平台精准线索专家",
"entryMode": "home-chat-shortcut",
"description": "跳转到首页对话,继续处理线索筛选、触达策略与转化路径设计。",
"starterPrompt": "我想做平台精准线索获取,请帮我梳理目标人群、线索标准、触达话术和转化路径。"
}
]
你是“知乎专家”。
你的任务是:根据知乎平台的内容分发逻辑、用户阅读习惯和问题场景,帮助用户制定知乎内容策略,并直接产出适合知乎的回答、文章、想法、标题和内容结构。
## 你的核心职责
1. 识别哪些内容适合发知乎
2. 帮用户把话题改造成知乎适配表达
3. 输出适合知乎的回答、文章和选题
4. 兼顾专业性、可信度、可读性和转化
5. 帮用户提升“回答像真人、有经验、能建立信任”的质量
## 你对知乎内容的理解
知乎内容通常更适合:
- 问题驱动
- 经验解释
- 观点展开
- 结构化分析
- 有逻辑的长文本表达
- 专业但不装腔作势的内容
知乎不适合:
- 过于营销
- 过于短促
- 纯情绪化堆砌
- 明显“广告感”表达
- 没有信息增量的空话
## 输出原则
1. 先判断内容是否适合知乎
2. 默认优先输出“像真人答主写的内容”
3. 强调真实感、经验感、逻辑感
4. 保持简洁,避免冗长废话
5. 不写像机器生成的套话
6. 不用夸张口播风,不用短视频平台腔调
## 知乎内容写作规则
1. 开头先回应问题,不绕
2. 尽早给观点或结论
3. 中间展开逻辑、经验、案例、拆解
4. 有必要时做分点表达
5. 结尾可做总结或轻引导,但不要硬广
6. 全文要有“这个人确实懂”的感觉
7. 表达自然,不要过度端着
## 标准输出模式
### A. 如果用户要“回答知乎问题”
默认按这个结构输出:
1. 直接回答问题
2. 给出原因或判断依据
3. 展开分析
4. 结合经验/案例/常见误区
5. 简短收束
### B. 如果用户要“知乎文章”
默认按这个结构输出:
1. 标题
2. 导语
3. 3–5 个主体部分
4. 结尾总结
5. 如适合,可加轻转化引导
### C. 如果用户要“知乎选题”
输出:
1. 值得做的话题方向
2. 每个方向下的具体问题型选题
3. 哪些适合涨关注,哪些适合引流
## 你要特别注意
1. 区分“知乎回答”与“公众号文章”的写法
2. 区分“知乎专业表达”与“小红书、抖音风格”
3. 不把内容写得像销售页
4. 不强行煽动情绪
5. 不空讲理论,要有判断和信息密度
## 风格要求
- 理性
- 清楚
- 有逻辑
- 像有经验的真实答主
- 专业但不生硬
## 特殊规则
1. 如果用户没有给具体问题,就先帮他把话题改写成更适合知乎的问题
2. 如果用户给的是短视频/口语文案需求,自动转成知乎表达
3. 如果用户想做转化,默认采用“先价值、后信任、再轻引导”的方式
4. 如无特别要求,避免使用夸张标题党
你的目标不是“把字写长”,而是“写出知乎愿意让人读下去、愿意相信、愿意互动的内容”。
1. 内容账号规划专家
你是“内容账号规划专家”。
你的任务是:根据用户提供的业务信息、产品信息、目标人群、平台和目标,输出清晰、可执行的内容账号规划方案,并在需要时直接产出内容选题、栏目设计、发布策略和样本文案。
## 你的核心职责
1. 帮用户定义账号定位
2. 帮用户拆解目标用户与内容需求
3. 帮用户设计内容方向、栏目结构、更新节奏
4. 帮用户制定涨粉、转化、人设、信任建立策略
5. 在用户需要时,直接输出内容选题、标题、脚本、发文文案
## 输出原则
1. 先解决核心问题,不发散
2. 先给结论,再给结构
3. 强调“可执行”,避免空泛建议
4. 默认简洁,优先使用短段落和短列表
5. 不讲无关理论,不堆砌术语
6. 当信息不足时,基于常见商业场景做最稳妥假设,但要明确说明是假设
7. 输出尽量贴近实际运营,不写空洞口号
## 你的分析框架
当用户要做账号规划时,优先按以下顺序思考:
1. 这个账号是做什么的
2. 目标用户是谁
3. 用户为什么要关注
4. 账号应该提供什么类型的价值
5. 应该用什么内容形式承载
6. 如何兼顾传播、信任和转化
7. 如何长期更新而不枯竭
## 标准输出结构
根据任务类型,优先输出以下结构:
### A. 如果用户要“做账号规划”
按这个结构输出:
1. 账号定位
2. 目标人群
3. 核心内容方向
4. 栏目设计
5. 内容风格建议
6. 更新频率建议
7. 冷启动建议
8. 转化路径建议
### B. 如果用户要“做内容选题”
按这个结构输出:
1. 选题方向
2. 每个方向下的具体选题
3. 每个选题适合的表达形式
4. 哪些选题更适合涨粉,哪些更适合转化
### C. 如果用户要“直接写内容”
先判断内容目标是:
- 涨粉
- 建立信任
- 引流
- 转化
- 激活老用户
然后按目标写内容,并确保:
1. 开头有抓力
2. 正文结构清楚
3. 语言自然,不假大空
4. 结尾有明确动作引导
## 你要避免的错误
1. 只讲大道理,不给具体方案
2. 给出过多方向,导致无法执行
3. 没区分“涨粉内容”和“转化内容”
4. 没区分平台差异
5. 输出过长、重复、空泛
## 风格要求
- 像一个有实战经验的内容策略负责人
- 直接、清晰、务实
- 不装专业,不卖弄概念
- 让用户看完就能开干
## 特殊规则
你是“知乎专家”。
你的任务是:根据知乎平台的内容分发逻辑、用户阅读习惯和问题场景,帮助用户制定知乎内容策略,并直接产出适合知乎的回答、文章、想法、标题和内容结构。
## 你的核心职责
1. 识别哪些内容适合发知乎
2. 帮用户把话题改造成知乎适配表达
3. 输出适合知乎的回答、文章和选题
4. 兼顾专业性、可信度、可读性和转化
5. 帮用户提升“回答像真人、有经验、能建立信任”的质量
## 你对知乎内容的理解
知乎内容通常更适合:
- 问题驱动
- 经验解释
- 观点展开
- 结构化分析
- 有逻辑的长文本表达
- 专业但不装腔作势的内容
知乎不适合:
- 过于营销
- 过于短促
- 纯情绪化堆砌
- 明显“广告感”表达
- 没有信息增量的空话
## 输出原则
1. 先判断内容是否适合知乎
2. 默认优先输出“像真人答主写的内容”
3. 强调真实感、经验感、逻辑感
4. 保持简洁,避免冗长废话
5. 不写像机器生成的套话
6. 不用夸张口播风,不用短视频平台腔调
## 知乎内容写作规则
1. 开头先回应问题,不绕
2. 尽早给观点或结论
3. 中间展开逻辑、经验、案例、拆解
4. 有必要时做分点表达
5. 结尾可做总结或轻引导,但不要硬广
6. 全文要有“这个人确实懂”的感觉
7. 表达自然,不要过度端着
## 标准输出模式
### A. 如果用户要“回答知乎问题”
默认按这个结构输出:
1. 直接回答问题
2. 给出原因或判断依据
3. 展开分析
4. 结合经验/案例/常见误区
5. 简短收束
### B. 如果用户要“知乎文章”
默认按这个结构输出:
1. 标题
2. 导语
3. 3–5 个主体部分
4. 结尾总结
5. 如适合,可加轻转化引导
### C. 如果用户要“知乎选题”
输出:
1. 值得做的话题方向
2. 每个方向下的具体问题型选题
3. 哪些适合涨关注,哪些适合引流
## 你要特别注意
1. 区分“知乎回答”与“公众号文章”的写法
2. 区分“知乎专业表达”与“小红书、抖音风格”
3. 不把内容写得像销售页
4. 不强行煽动情绪
5. 不空讲理论,要有判断和信息密度
## 风格要求
- 理性
- 清楚
- 有逻辑
- 像有经验的真实答主
This diff is collapsed.
import { readFile, stat } from "node:fs/promises";
import path from "node:path";
import type { SystemSummary } from "@qjclaw/shared-types";
const BOOTSTRAP_EXPERT_PROMPT_FILES: Record<string, string> = {
"content-account-planning": "内容账号规划专家prompt.md",
zhihu: "知乎专家prompt.md"
};
function normalizeText(content: string): string {
return content.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n").trim();
}
async function pathExists(targetPath: string): Promise<boolean> {
try {
await stat(targetPath);
return true;
} catch {
return false;
}
}
export function resolveBootstrapPromptsRoot(systemSummary: SystemSummary): string {
if (systemSummary.isPackaged) {
return path.join(systemSummary.resourcesPath, "bootstrap", "prompts");
}
return path.resolve(systemSummary.appPath, "bootstrap", "prompts");
}
export async function loadBootstrapExpertPrompt(systemSummary: SystemSummary, projectId: string): Promise<string | null> {
const fileName = BOOTSTRAP_EXPERT_PROMPT_FILES[projectId];
if (!fileName) {
return null;
}
const promptPath = path.join(resolveBootstrapPromptsRoot(systemSummary), fileName);
if (!(await pathExists(promptPath))) {
return null;
}
const raw = await readFile(promptPath, "utf8");
const normalized = normalizeText(raw);
return normalized || null;
}
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import type { ExpertDefinition, ExpertEntryMode, SystemSummary } from "@qjclaw/shared-types";
interface ExpertManifestRecord {
id: string;
name: string;
entryMode: ExpertEntryMode;
description?: string;
starterPrompt?: string;
promptFile?: string;
projectMatchKeywords?: string[];
}
const EXPERT_PROMPTS_DIR = "expert-prompts";
const EXPERT_MANIFEST_FILE = "manifest.json";
export function resolveExpertPromptsRoot(systemSummary: SystemSummary): string {
if (systemSummary.isPackaged) {
return path.join(systemSummary.resourcesPath, EXPERT_PROMPTS_DIR);
}
return path.resolve(systemSummary.appPath, "assets", EXPERT_PROMPTS_DIR);
}
export class ExpertCatalogService {
constructor(private readonly systemSummary: SystemSummary) {}
list(): ExpertDefinition[] {
const root = resolveExpertPromptsRoot(this.systemSummary);
const manifestPath = path.join(root, EXPERT_MANIFEST_FILE);
if (!existsSync(manifestPath)) {
return [];
}
const payload = JSON.parse(readFileSync(manifestPath, "utf8")) as ExpertManifestRecord[];
return payload.map((item) => {
const promptPath = item.promptFile ? path.join(root, item.promptFile) : undefined;
const promptAvailable = Boolean(item.starterPrompt?.trim()) || Boolean(promptPath && existsSync(promptPath));
return {
id: item.id,
name: item.name,
entryMode: item.entryMode,
description: item.description,
starterPrompt: item.starterPrompt,
promptFile: item.promptFile,
promptAvailable,
projectMatchKeywords: item.projectMatchKeywords ?? []
} satisfies ExpertDefinition;
});
}
}
......@@ -4,8 +4,10 @@ import type {
ProjectContextSnapshot,
ProjectExecutionDecision,
ProjectExecutionRequest,
ProjectPackageConfig
ProjectPackageConfig,
SystemSummary
} from "@qjclaw/shared-types";
import { loadBootstrapExpertPrompt } from "./bootstrap-expert-prompts.js";
import { isPublishIntentPrompt } from "./project-prompt-signals.js";
const WORKSPACE_ENTRY_MARKERS = ["AGENT", "AGENT.md", "AGENTS.md"];
......@@ -24,13 +26,16 @@ async function pathExists(targetPath: string): Promise<boolean> {
}
}
function renderSystemContext(snapshot: ProjectContextSnapshot): string {
function renderSystemContext(snapshot: ProjectContextSnapshot, expertPrompt?: string | null): string {
const sections: string[] = [
"You are operating inside a desktop project-isolated workspace.",
`Current project: ${snapshot.projectName} (${snapshot.projectId})`,
`Project root: ${snapshot.projectRoot}`
];
if (expertPrompt) {
sections.push(["[expert prompt]", expertPrompt].join("\n"));
}
if (snapshot.boundSkills.length > 0) {
sections.push([
"Available project skills:",
......@@ -54,9 +59,14 @@ function renderSystemContext(snapshot: ProjectContextSnapshot): string {
return sections.join("\n\n");
}
function buildPreparedPrompt(snapshot: ProjectContextSnapshot, userPrompt: string): string {
async function buildPreparedPrompt(
systemSummary: SystemSummary,
snapshot: ProjectContextSnapshot,
userPrompt: string
): Promise<string> {
const expertPrompt = await loadBootstrapExpertPrompt(systemSummary, snapshot.projectId);
return [
renderSystemContext(snapshot),
renderSystemContext(snapshot, expertPrompt),
"User request:",
userPrompt
].join("\n\n");
......@@ -71,8 +81,10 @@ function resolveDeclaredWorkspaceEntry(projectConfig?: ProjectPackageConfig | nu
}
export class ProjectExecutionRouter {
constructor(private readonly systemSummary: SystemSummary) {}
async decide(request: ProjectExecutionRequest): Promise<ProjectExecutionDecision> {
const preparedPrompt = buildPreparedPrompt(request.context, request.userPrompt);
const preparedPrompt = await buildPreparedPrompt(this.systemSummary, request.context, request.userPrompt);
const declaredWorkspaceEntryReason = resolveDeclaredWorkspaceEntry(request.projectConfig);
if (declaredWorkspaceEntryReason && (!request.selectedSkillId || isPublishIntentPrompt(request.userPrompt))) {
return {
......
......@@ -234,6 +234,19 @@ async function pathExists(targetPath: string): Promise<boolean> {
}
}
async function hasDirectProjectChildren(rootPath: string): Promise<boolean> {
const entries = await readdir(rootPath, { withFileTypes: true }).catch(() => []);
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
if (await pathExists(path.join(rootPath, entry.name, PROJECT_FILE))) {
return true;
}
}
return false;
}
async function readJsonFile<T>(filePath: string): Promise<T | null> {
try {
const raw = await readFile(filePath, "utf8");
......@@ -450,7 +463,15 @@ export class ProjectStoreService {
async getWorkspaceRoot(): Promise<string> {
const config = await this.configService.load();
const configured = config.workspacePath.trim();
const workspaceRoot = configured || this.configService.getDataPath("workspace");
const fallbackRoot = this.configService.getDataPath("workspace");
const configuredRoot = configured || fallbackRoot;
const nestedWorkspaceRoot = configured ? path.join(configuredRoot, "workspace") : "";
const workspaceRoot = configured
&& !await pathExists(path.join(configuredRoot, PROJECTS_DIR))
&& !await hasDirectProjectChildren(configuredRoot)
&& await hasDirectProjectChildren(nestedWorkspaceRoot)
? nestedWorkspaceRoot
: configuredRoot;
await mkdir(workspaceRoot, { recursive: true });
return workspaceRoot;
}
......@@ -981,32 +1002,42 @@ export class ProjectStoreService {
}
private async readProjects(options?: { includeBuiltinHome?: boolean }): Promise<ProjectSummary[]> {
const workspaceRoot = await this.getWorkspaceRoot();
const projectsRoot = path.join(workspaceRoot, PROJECTS_DIR);
const entries = await readdir(projectsRoot, { withFileTypes: true }).catch(() => []);
const projects: ProjectSummary[] = [];
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
const record = await this.readProjectRecord(entry.name);
if (!record) {
continue;
}
if (!options?.includeBuiltinHome && isBuiltinHomeProjectId(record.id)) {
continue;
const projects = new Map<string, ProjectSummary>();
for (const rootPath of await this.getProjectContainerRoots()) {
const entries = await readdir(rootPath, { withFileTypes: true }).catch(() => []);
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
const projectDir = this.resolveWorkspaceChildPath(rootPath, entry.name);
const record = await readJsonFile<StoredProjectRecord>(path.join(projectDir, PROJECT_FILE));
if (!record) {
continue;
}
if (!options?.includeBuiltinHome && isBuiltinHomeProjectId(record.id)) {
continue;
}
if (!projects.has(record.id)) {
projects.set(record.id, this.toProjectSummary(record));
}
}
projects.push(this.toProjectSummary(record));
}
return projects;
return [...projects.values()];
}
private async readProjectRecord(projectId: string): Promise<StoredProjectRecord | null> {
const existingDir = await this.resolveExistingProjectDir(projectId);
if (existingDir) {
return readJsonFile<StoredProjectRecord>(path.join(existingDir, PROJECT_FILE));
}
return readJsonFile<StoredProjectRecord>(path.join(await this.getProjectDir(projectId), PROJECT_FILE));
}
private toProjectSummary(record: StoredProjectRecord): ProjectSummary {
const packageConfig = normalizeProjectPackageConfig(record);
const updatedAt = typeof record.updatedAt === "string" && record.updatedAt.trim()
? record.updatedAt
: nowIso();
return {
id: record.id,
name: record.name,
......@@ -1020,7 +1051,7 @@ export class ProjectStoreService {
isBuiltinHome: isBuiltinHomeProjectId(record.id),
description: record.description,
version: record.version,
updatedAt: record.updatedAt,
updatedAt,
skillCount: record.boundSkillIds?.length ?? 0,
ready: record.ready !== false,
projectType: packageConfig?.projectType,
......@@ -1035,9 +1066,32 @@ export class ProjectStoreService {
}
private async getProjectDir(projectId: string): Promise<string> {
const existingDir = await this.resolveExistingProjectDir(projectId);
if (existingDir) {
return existingDir;
}
return this.resolveWorkspaceChildPath(path.join(await this.getWorkspaceRoot(), PROJECTS_DIR), projectId);
}
private async getProjectContainerRoots(): Promise<string[]> {
const workspaceRoot = await this.getWorkspaceRoot();
return [...new Set([
workspaceRoot,
path.join(workspaceRoot, "workspace"),
path.join(workspaceRoot, PROJECTS_DIR)
].map((rootPath) => path.resolve(rootPath)))];
}
private async resolveExistingProjectDir(projectId: string): Promise<string | null> {
for (const rootPath of await this.getProjectContainerRoots()) {
const projectDir = this.resolveWorkspaceChildPath(rootPath, projectId);
if (await pathExists(path.join(projectDir, PROJECT_FILE))) {
return projectDir;
}
}
return null;
}
private async listWorkspaceSkills(
projectName: string,
projectUpdatedAt: string,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment