347 lines
14 KiB
TypeScript
347 lines
14 KiB
TypeScript
import type {
|
||
ActorCard,
|
||
BehaviorDriverCard,
|
||
MainStepKey,
|
||
NarrativeCard,
|
||
PurposeCard,
|
||
SixStepMessage,
|
||
StepStatus,
|
||
SubStepKey,
|
||
} from './types';
|
||
|
||
const PURPOSE_SEED: { title: string; reason: string }[] = [
|
||
{ title: '建立品牌权威认知', reason: '通过持续的专业内容输出,塑造行业领导者形象,使核心受众在面对同类品牌时优先想起本品牌,并带来长期信任度' },
|
||
{ title: '激活核心用户决策路径', reason: '梳理用户从兴趣触达到决策转化的关键触点,缩短决策链路、提升转化效率' },
|
||
{ title: '强化情感价值连接', reason: '通过故事化叙事激发情绪共鸣,沉淀难以被替代的品牌情感资产' },
|
||
];
|
||
|
||
const ACTOR_SEED: { title: string; content: string }[] = [
|
||
{ title: '行业意见领袖(KOL)', content: '在专业垂直领域有持续影响力,可在第一时间撬动核心圈层并带动二次扩散' },
|
||
{ title: '品牌官方账号矩阵', content: '统一对外传递品牌主张与最新动态,承担信息发布、用户互动与口碑响应' },
|
||
{ title: '深度内容创作者', content: '产出长图文、视频解读等内容形态,承担引发讨论与口碑沉淀的角色' },
|
||
{ title: '行业垂直媒体', content: '具备专业背书能力,承担议题设置与权威背书职责' },
|
||
{ title: '核心用户共创社区', content: '由忠诚用户构成,通过 UGC 形式输出真实使用反馈与场景示范' },
|
||
{ title: '行业分析机构', content: '通过研究报告与数据支撑品牌主张,加强观点的可信度' },
|
||
{ title: '品牌代言关键人', content: '与品牌价值观高度契合的代言人或合作方,承担情感投射与信任放大' },
|
||
];
|
||
|
||
const BEHAVIOR_SEED: { expectedBehavior: string; driver: string }[] = [
|
||
{ expectedBehavior: '主动转发并附带专业点评', driver: '内容契合自身专业立场,带来圈层认同与社交资本' },
|
||
{ expectedBehavior: '在专业社区发起延伸讨论', driver: '议题具备争议或洞察价值,能引发同行参与并扩散' },
|
||
{ expectedBehavior: '将品牌内容沉淀为参考资料', driver: '内容具备结构化、可复用的方法论价值,可在工作中长期引用' },
|
||
{ expectedBehavior: '主动在直播间或路演中引用品牌观点', driver: '观点具备可解释性,能帮助行为体强化自身专业可信度' },
|
||
{ expectedBehavior: '产出二次内容(长图文 / 视频)', driver: '原始素材具备可改编空间,能匹配自身受众语境' },
|
||
{ expectedBehavior: '组织线下闭门交流会', driver: '议题对核心受众具备稀缺性,符合社群运营节奏' },
|
||
{ expectedBehavior: '在调研报告中将品牌列为典型案例', driver: '品牌案例具备数据支撑、典型且可被结构化引用' },
|
||
{ expectedBehavior: '在公开演讲中引用品牌叙事框架', driver: '叙事框架具备方法论高度,可被借用以提升自身演讲含金量' },
|
||
];
|
||
|
||
const NARRATIVE_SEED: { title: string; reason: string }[] = [
|
||
{ title: '从噪音中识别真实信号', reason: '聚焦行业过度信息的痛点,主张以理性框架回归本质' },
|
||
{ title: '让专业判断回到决策中心', reason: '强调专业判断在复杂环境下的不可替代性' },
|
||
{ title: '以长期主义穿越周期', reason: '倡导关注长期价值积累,而非短期爆点' },
|
||
{ title: '把复杂问题讲简单', reason: '通过结构化表达让专业内容更易被传播与理解' },
|
||
{ title: '让每一次选择都有迹可循', reason: '强调决策可追溯、可复盘,建立专业信任' },
|
||
{ title: '与时间做朋友', reason: '把品牌成长锚定在长期视角,淡化短期波动焦虑' },
|
||
{ title: '相信专业的力量', reason: '通过行为体共同的专业身份认同强化品牌主张' },
|
||
{ title: '把方法论变成可复用的工具', reason: '将抽象主张沉淀为工具化资产,方便受众反复引用' },
|
||
];
|
||
|
||
const PURPOSE_THINKING = `根据当前问题语境,需要先厘清品牌战略推演的核心目的:
|
||
|
||
1. 综合输入素材,识别可能的目标方向;
|
||
2. 评估不同目的之间的优先级与互补性;
|
||
3. 选择最契合当前阶段的目的组合,作为后续推演基础。`;
|
||
|
||
const ACTOR_THINKING = `围绕已生成的目的,开始识别支撑该目的所需的关键行为体:
|
||
|
||
- 需要明确每个行为体的角色定位
|
||
- 评估其对目的的贡献度与覆盖范围
|
||
- 避免行为体之间能力重叠或缺位`;
|
||
|
||
const BEHAVIOR_THINKING = `针对每个行为体,推演其在场景中应当展现的预期行为:
|
||
|
||
- 行为需要可观察、可衡量
|
||
- 同时识别推动该行为发生的关键驱动因素
|
||
- 行为与驱动因素需保持因果一致性`;
|
||
|
||
const NARRATIVE_THINKING = `基于驱动因素,构建用于触发行为的叙事框架:
|
||
|
||
- 叙事需要紧扣驱动因素的情绪或理性触点
|
||
- 兼顾品牌主张与受众心智匹配度
|
||
- 形成可被复用的叙事模板`;
|
||
|
||
export interface SixStepMockController {
|
||
message: SixStepMessage;
|
||
stop: () => void;
|
||
promise: Promise<void>;
|
||
}
|
||
|
||
const TYPE_INTERVAL = 28;
|
||
const STAGE_GAP = 380;
|
||
|
||
function buildPurposeCards(): PurposeCard[] {
|
||
return PURPOSE_SEED.map((seed, idx) => ({
|
||
id: `purpose-${idx + 1}`,
|
||
index: idx + 1,
|
||
title: seed.title,
|
||
reason: seed.reason,
|
||
}));
|
||
}
|
||
|
||
function buildActorCards(purposes: PurposeCard[]): ActorCard[] {
|
||
const cards: ActorCard[] = [];
|
||
let counter = 0;
|
||
purposes.forEach((purpose, pIdx) => {
|
||
const count = pIdx === 0 ? 2 : 1;
|
||
for (let i = 0; i < count; i += 1) {
|
||
counter += 1;
|
||
const seed = ACTOR_SEED[(counter - 1) % ACTOR_SEED.length];
|
||
cards.push({
|
||
id: `actor-${counter}`,
|
||
index: counter,
|
||
purposeId: purpose.id,
|
||
title: seed.title,
|
||
content: seed.content,
|
||
});
|
||
}
|
||
});
|
||
return cards;
|
||
}
|
||
|
||
function buildBehaviorCards(actors: ActorCard[]): BehaviorDriverCard[] {
|
||
const cards: BehaviorDriverCard[] = [];
|
||
let counter = 0;
|
||
actors.forEach((actor, aIdx) => {
|
||
const count = aIdx % 2 === 0 ? 2 : 1;
|
||
for (let i = 0; i < count; i += 1) {
|
||
counter += 1;
|
||
const seed = BEHAVIOR_SEED[(counter - 1) % BEHAVIOR_SEED.length];
|
||
cards.push({
|
||
id: `behavior-${counter}`,
|
||
index: counter,
|
||
actorId: actor.id,
|
||
expectedBehavior: seed.expectedBehavior,
|
||
driver: seed.driver,
|
||
});
|
||
}
|
||
});
|
||
return cards;
|
||
}
|
||
|
||
function buildNarrativeCards(behaviors: BehaviorDriverCard[]): NarrativeCard[] {
|
||
const cards: NarrativeCard[] = [];
|
||
let counter = 0;
|
||
behaviors.forEach((behavior, bIdx) => {
|
||
const count = bIdx % 2 === 0 ? 2 : 1;
|
||
for (let i = 0; i < count; i += 1) {
|
||
counter += 1;
|
||
const seed = NARRATIVE_SEED[(counter - 1) % NARRATIVE_SEED.length];
|
||
cards.push({
|
||
id: `narrative-${counter}`,
|
||
index: counter,
|
||
driverId: behavior.id,
|
||
title: seed.title,
|
||
reason: seed.reason,
|
||
});
|
||
}
|
||
});
|
||
return cards;
|
||
}
|
||
|
||
function buildFinalAnswer(): string {
|
||
return `综合本次 6 步法推演结果,可以得到如下战略要点:
|
||
|
||
1. **目的**:以品牌权威认知为锚点,在专业人群中建立可信表达。
|
||
2. **行为体**:依靠 KOL、官方账号矩阵、深度创作者三类协同发声。
|
||
3. **预期行为与驱动因素**:通过专业内容激发主动传播与延伸讨论,形成圈层共振。
|
||
4. **叙事**:以"在噪音中识别真实信号"为核心叙事线,串联整个传播节奏。
|
||
|
||
后续可基于该推演继续生成传播节奏、渠道分工与内容资产清单。`;
|
||
}
|
||
|
||
export function buildInitialAssistantMessage(): SixStepMessage {
|
||
return {
|
||
id: `assistant-${Date.now()}`,
|
||
role: 'assistant',
|
||
content: '',
|
||
timestamp: new Date(),
|
||
stages: {},
|
||
finalAnswer: '',
|
||
mainStepStatus: { info: 'pending', sixstep: 'pending', final: 'pending' },
|
||
subStepStatus: { purpose: 'pending', actor: 'pending', behavior: 'pending', narrative: 'pending' },
|
||
};
|
||
}
|
||
|
||
interface RunOptions {
|
||
onUpdate: (message: SixStepMessage) => void;
|
||
onComplete?: () => void;
|
||
}
|
||
|
||
/**
|
||
* 模拟 6 步法推演的全过程:信息准备 → 6 步法(4 个阶段) → 最终结果。
|
||
*
|
||
* 通过分阶段写入 thinking 文本和卡片,模拟真实流式生成效果。
|
||
*/
|
||
export function runSixStepDeduction(message: SixStepMessage, options: RunOptions): SixStepMockController {
|
||
let cancelled = false;
|
||
const timers: number[] = [];
|
||
|
||
const wait = (ms: number) =>
|
||
new Promise<void>((resolve) => {
|
||
const id = window.setTimeout(() => {
|
||
resolve();
|
||
}, ms);
|
||
timers.push(id);
|
||
});
|
||
|
||
const setMain = (key: MainStepKey, status: StepStatus) => {
|
||
message.mainStepStatus = { ...(message.mainStepStatus ?? { info: 'pending', sixstep: 'pending', final: 'pending' }), [key]: status };
|
||
if (status === 'running') {
|
||
message.currentMainStep = key;
|
||
}
|
||
};
|
||
|
||
const setSub = (key: SubStepKey, status: StepStatus) => {
|
||
message.subStepStatus = {
|
||
...(message.subStepStatus ?? { purpose: 'pending', actor: 'pending', behavior: 'pending', narrative: 'pending' }),
|
||
[key]: status,
|
||
};
|
||
if (status === 'running') {
|
||
message.currentSubStep = key;
|
||
}
|
||
};
|
||
|
||
const emit = () => {
|
||
if (cancelled) return;
|
||
options.onUpdate({ ...message, stages: { ...message.stages } });
|
||
};
|
||
|
||
const typeInto = async (text: string, write: (chunk: string) => void) => {
|
||
const step = 4;
|
||
for (let i = 0; i <= text.length; i += step) {
|
||
if (cancelled) return;
|
||
write(text.slice(0, i));
|
||
emit();
|
||
await wait(TYPE_INTERVAL);
|
||
}
|
||
if (!cancelled) {
|
||
write(text);
|
||
emit();
|
||
}
|
||
};
|
||
|
||
const promise = (async () => {
|
||
setMain('info', 'running');
|
||
emit();
|
||
await wait(800);
|
||
if (cancelled) return;
|
||
setMain('info', 'done');
|
||
setMain('sixstep', 'running');
|
||
emit();
|
||
await wait(STAGE_GAP);
|
||
|
||
setSub('purpose', 'running');
|
||
message.stages = { ...message.stages, purpose: { thinking: '', cards: [], complete: false } };
|
||
emit();
|
||
await typeInto(PURPOSE_THINKING, (chunk) => {
|
||
message.stages = { ...message.stages, purpose: { ...(message.stages?.purpose as any), thinking: chunk } };
|
||
});
|
||
if (cancelled) return;
|
||
const purposes = buildPurposeCards();
|
||
for (const card of purposes) {
|
||
if (cancelled) return;
|
||
const existing = message.stages?.purpose?.cards ?? [];
|
||
message.stages = { ...message.stages, purpose: { ...(message.stages?.purpose as any), cards: [...existing, card] } };
|
||
emit();
|
||
await wait(280);
|
||
}
|
||
message.stages = { ...message.stages, purpose: { ...(message.stages?.purpose as any), complete: true } };
|
||
setSub('purpose', 'done');
|
||
emit();
|
||
await wait(STAGE_GAP);
|
||
|
||
setSub('actor', 'running');
|
||
message.stages = { ...message.stages, actor: { thinking: '', cards: [], complete: false } };
|
||
emit();
|
||
await typeInto(ACTOR_THINKING, (chunk) => {
|
||
message.stages = { ...message.stages, actor: { ...(message.stages?.actor as any), thinking: chunk } };
|
||
});
|
||
if (cancelled) return;
|
||
const actors = buildActorCards(purposes);
|
||
for (const card of actors) {
|
||
if (cancelled) return;
|
||
const existing = message.stages?.actor?.cards ?? [];
|
||
message.stages = { ...message.stages, actor: { ...(message.stages?.actor as any), cards: [...existing, card] } };
|
||
emit();
|
||
await wait(220);
|
||
}
|
||
message.stages = { ...message.stages, actor: { ...(message.stages?.actor as any), complete: true } };
|
||
setSub('actor', 'done');
|
||
emit();
|
||
await wait(STAGE_GAP);
|
||
|
||
setSub('behavior', 'running');
|
||
message.stages = { ...message.stages, behavior: { thinking: '', cards: [], complete: false } };
|
||
emit();
|
||
await typeInto(BEHAVIOR_THINKING, (chunk) => {
|
||
message.stages = { ...message.stages, behavior: { ...(message.stages?.behavior as any), thinking: chunk } };
|
||
});
|
||
if (cancelled) return;
|
||
const behaviors = buildBehaviorCards(actors);
|
||
for (const card of behaviors) {
|
||
if (cancelled) return;
|
||
const existing = message.stages?.behavior?.cards ?? [];
|
||
message.stages = { ...message.stages, behavior: { ...(message.stages?.behavior as any), cards: [...existing, card] } };
|
||
emit();
|
||
await wait(200);
|
||
}
|
||
message.stages = { ...message.stages, behavior: { ...(message.stages?.behavior as any), complete: true } };
|
||
setSub('behavior', 'done');
|
||
emit();
|
||
await wait(STAGE_GAP);
|
||
|
||
setSub('narrative', 'running');
|
||
message.stages = { ...message.stages, narrative: { thinking: '', cards: [], complete: false } };
|
||
emit();
|
||
await typeInto(NARRATIVE_THINKING, (chunk) => {
|
||
message.stages = { ...message.stages, narrative: { ...(message.stages?.narrative as any), thinking: chunk } };
|
||
});
|
||
if (cancelled) return;
|
||
const narratives = buildNarrativeCards(behaviors);
|
||
for (const card of narratives) {
|
||
if (cancelled) return;
|
||
const existing = message.stages?.narrative?.cards ?? [];
|
||
message.stages = { ...message.stages, narrative: { ...(message.stages?.narrative as any), cards: [...existing, card] } };
|
||
emit();
|
||
await wait(200);
|
||
}
|
||
message.stages = { ...message.stages, narrative: { ...(message.stages?.narrative as any), complete: true } };
|
||
setSub('narrative', 'done');
|
||
setMain('sixstep', 'done');
|
||
setMain('final', 'running');
|
||
emit();
|
||
await wait(STAGE_GAP);
|
||
|
||
if (cancelled) return;
|
||
await typeInto(buildFinalAnswer(), (chunk) => {
|
||
message.finalAnswer = chunk;
|
||
});
|
||
if (cancelled) return;
|
||
setMain('final', 'done');
|
||
message.currentMainStep = undefined;
|
||
message.currentSubStep = undefined;
|
||
emit();
|
||
options.onComplete?.();
|
||
})();
|
||
|
||
return {
|
||
message,
|
||
stop: () => {
|
||
cancelled = true;
|
||
timers.forEach((t) => window.clearTimeout(t));
|
||
},
|
||
promise,
|
||
};
|
||
}
|