Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc [SEC-CLEAN] · pre-push-clean v1.0 · 109倿æä¿¡æ¯å·²èªåšèœ¬ä¹±ç
38 KiB
belongs_to
| belongs_to | |
|---|---|
|
ð éžæžæä»€ïœAIåçœæå¹³å°Â·Phase A · ç åå° Â· AIèŸ å©åäœçŒèŸåš + å šçšçç + AIäœ¿çšæ¥åïŒZY-WRITING-EDITOR-2026-0325-002ïŒÂ· éç çŸå · å°æææ
ããæä»€æ»è§
æ¬æä»€èŠæ±éžæžåš guanghulab.com/writing/editor/ äžæå»º ãç åå°ã ââ çœæè¡äžäžçšAIèŸ
å©åäœçŒèŸåšïŒå®ç°åå€§æ žå¿ïŒ
- 坿æ¬åäœçŒèŸåš â åºäº Slate.js ççœæäžçšçŒèŸåšïŒç« è管çãåæ°ç»è®¡ãèªåšä¿åïŒ
- AIèŸ å©åäœ â éäžææ¬è°AIïŒæ©å/ç»å/æ¹å/çµæïŒïŒäŸ§èŸ¹æ AI对è¯
- å šçšççç³»ç» â æ¯äžæ¬¡AIä»å ¥éœè®°åœïŒæ¶éŽãæäœãåæâæ°æãé纳/æç»ïŒ
- AIäœ¿çšæ¥å â èªåšçæABCD级æ¥åïŒA=åAIçâ€10% â D=åAIç>50%ïŒ
æ žå¿è®Ÿè®¡ç念
äžãææ¯éå
| å±çº§ | ææ¯ | çæ¬ | çšé |
|---|---|---|---|
| 坿æ¬çŒèŸåš | Slate.js | 0.100+ | æ žå¿åäœçŒèŸåšïŒå¯æä»¶ãå¯å®å¶ãäžæåå¥œïŒ |
| 坿æ¬å·¥å ·æ | Slate + èªå®ä¹æä»¶ | - | å ç²/æäœ/æ é¢/åå²çº¿/ç« è跳蜬 |
| åç«¯æ¡æ¶ | React 18 + TypeScript | 18.2+ | UI ç»ä»¶åïŒäž Phase B å ±çšïŒ |
| æ ·åŒ | Tailwind CSS | 3.3+ | ååºåŒ + äžæ³šæš¡åŒæè²äž»é¢ |
| AIè°çš | OpenAI SDK + Anthropic SDK | latest | èŸ å©åäœïŒæ©å/ç»å/æ¹å/çµæïŒ |
| 宿¶éä¿¡ | Socket.io | 4.x | AIæµåŒèŸåº + èªåšä¿å |
| æ°æ®ååš | Notion APIïŒäž»ïŒ+ IndexedDBïŒæ¬å°çŒåïŒ | - | äœåååš + 犻线çŒå |
| ççååš | Notion API + JSON | - | AIæäœæ¥å¿å šéååš |
| æ¥åçæ | AI + æ°æ®ç»è®¡ | - | èªåšçæABCD级AIäœ¿çšæ¥å |
äºãä»åºè·¯åŸè§å
åšäž»ä»åº qinfendebingshuo/guanghulab ç writing-platform/ äžæ©å±ïŒ
writing-platform/
âââ frontend/
â âââ src/
â âââ pages/
â â âââ EditorPage.tsx â ç åå°äž»é¡µé¢
â â âââ WorkListPage.tsx â äœåå衚页
â â âââ AIReportPage.tsx â AIäœ¿çšæ¥åæ¥ç页
â âââ editor/ â ç åå°æ žå¿æš¡å
â â âââ SlateEditor.tsx â Slate.js 坿æ¬çŒèŸåšäž»ç»ä»¶
â â âââ Toolbar.tsx â å·¥å
·æ ïŒæ ŒåŒå + AIåèœïŒ
â â âââ ChapterSidebar.tsx â å·ŠäŸ§ç« èå¯Œèªæ
â â âââ AIPanel.tsx â å³äŸ§AI婿颿¿
â â âââ AIFloatingMenu.tsx â éäžææ¬åAIæµ®åšèå
â â âââ WordCounter.tsx â åæ°ç»è®¡æ
â â âââ TraceLog.tsx â ççæ¥å¿é¢æ¿
â â âââ AIReportCard.tsx â AIäœ¿çšæ¥åå¡ç
â â âââ plugins/
â â â âââ withChapters.ts â ç« èæä»¶ïŒèªåšç« èæ£æµã跳蜬ïŒ
â â â âââ withAutoSave.ts â èªåšä¿åæä»¶ïŒ30séŽé + 倱çŠä¿åïŒ
â â â âââ withWordCount.ts â åæ°ç»è®¡æä»¶ïŒå®æ¶ç»è®¡ïŒ
â â â âââ withAITrace.ts â AIççæä»¶ïŒæ¯æ¬¡AIæäœèªåšè®°åœïŒ
â â â âââ withMarkdownShortcuts.ts â Markdown å¿«æ·é®
â â âââ elements/
â â â âââ ChapterElement.tsx â ç« èæ é¢å
çŽ
â â â âââ ParagraphElement.tsx â æ®µèœå
çŽ
â â â âââ AIHighlight.tsx â AIä¿®æ¹é«äº®æ è®°
â â â âââ DividerElement.tsx â åºæ¯åå²çº¿
â â âââ utils/
â â âââ serializer.ts â Slate â çº¯ææ¬ / HTML / Markdown
â â âââ traceManager.ts â ççæ°æ®ç®¡ç
â â âââ reportGenerator.ts â AIäœ¿çšæ¥åçæåš
â â âââ chapterDetector.ts â ç« èèªå𿣿µ
â âââ hooks/
â â âââ useEditor.ts â çŒèŸåšç¶æç®¡ç
â â âââ useAIAssist.ts â AIèŸ
å©åäœ Hook
â â âââ useTrace.ts â ççç³»ç» Hook
â â âââ useAutoSave.ts â èªåšä¿å Hook
â âââ services/
â âââ editorApi.ts â äœåCRUD API
â âââ aiWritingApi.ts â AIåäœ API
â âââ traceApi.ts â ççæ°æ® API
â
âââ backend/
âââ src/
âââ routes/
â âââ works.ts â äœå管çïŒå建/å衚/读å/æŽæ°ïŒ
â âââ chapters.ts â ç« è管çïŒCRUD + æåºïŒ
â âââ aiWriting.ts â AIåäœæ¥å£ïŒæ©å/ç»å/æ¹å/çµæïŒ
â âââ traces.ts â ççè®°åœæ¥å£
â âââ reports.ts â AIäœ¿çšæ¥åæ¥å£
âââ services/
âââ workService.ts â äœåæ°æ®æå¡
âââ aiWritingService.ts â AIåäœæå¡ïŒå€æš¡åè°åºŠïŒ
âââ traceService.ts â ççååšæå¡
âââ reportService.ts â æ¥åçææå¡
â¹ïž äžç°æç åå°ç®åœçå ³ç³»ïŒä»åºäžå·²æ
writing-workspace/ïŒDEV-011 ååé£å¹ŽïŒïŒè¯¥ç®åœäžºæ©æç¬ç«æš¡åãæ¬æ¬¡åšwriting-platform/äžç»äžæå»ºïŒå°writing-workspace/äžå¯å€çšç代ç è¿å ¥ã
äžãPhase 0 · 坿æ¬åäœçŒèŸåš
3.1 çé¢åžå±
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â ð ãéžæ»çå°åšåŠ»ã åæ°: 45,230 仿¥: +2,180 [ð€ AI] [ð] [â è¿å] â
ââââââââââââ¬âââââââââââââââââââââââââââââââââââââââââââââ¬ââââââââââââââââââââââ€
â ç« èå¯Œèª â â ð€ AI婿 â
â â ââââââââââââââââââââââââââââââââââââââââ â â
â 第1ç« çžéâ â **å·¥å
·æ ** â â ç¬çµïŒ â
â 1,850å â â B I U | H1 H2 | â | ð€ | ⺠⻠â â è¿æ®µå¯¹è¯çèå¥ â
â 第2ç« éé¢â ââââââââââââââââââââââââââââââââââââââââ â å¯ä»¥åå¿«äžç¹ïŒ â
â 2,100å â â 建议猩ç䞺... â
â 第3ç« è¯¯è§£â ã仿¬èµ·å€ŽïŒçå°å¥¹ç«åšéšå£ãç¯å
æåš â â
â 1,900å â 她身äžïŒåäžå±æåçéç²ãã â âââââââââââââââ â
â 第4ç« ççžâ â ççè®°åœ (12) â
â 2,300å â ãäœ æä¹åšè¿å¿ïŒãä»ç声鳿äºå²åã â â 21:05 æ©å +120åâ
â ⶠ第5ç« ..â â â 21:03 ç»å +85å â
â 580å â ãæ...ã她欲èšåæ¢ïŒâ â â â 20:58 æ¹å 38å â
â âïžå·²å®ç» â â â
â â [éäžææ¬ååŒ¹åº AI æµ®åšèå] â âââââââââââââââ â
â + æ°ç« è â ââââââââââââââââââââââââââ â AIæ¥åæŠè§ â
â â â âšæ©å | âïžç»å | ðæ¹å â â ç级: **A** â
â â â ð¡çµæ | ðæèŠ | âé®ç¬çµâ â åAIç: 8.2% â
â â ââââââââââââââââââââââââââ â AIæäœ: 12次 â
â â â é纳ç: 75% â
ââââââââââââŽâââââââââââââââââââââââââââââââââââââââââââââŽââââââââââââââââââââââ€
â äžæ¬¡ä¿å: 21:08 | èªåšä¿å: åŒ | äžæ³šæš¡åŒ: å
³ | å¿«æ·é®: Ctrl+S ä¿å Ctrl+/ åŒAI â
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
åžå±è¯ŽæïŒ
- 巊䟧 · ç« èå¯Œèª â æŸç€ºææç« è + åæ° + åœåçŒèŸç« èé«äº®ïŒå¯ææœæåº
- äžéŽ Â· çŒèŸåº â äž»åäœåºåïŒçº¯åæ å¹²æ°ïŒç±»äŒŒè¯é/Wordåäœäœéª
- å³äŸ§ · AI婿 â AIå¯¹è¯ + ççè®°åœ + AIæ¥åæŠè§ïŒå¯æå
- åºéšç¶ææ â ä¿åç¶æ + äžæ³šæš¡åŒåŒå ³ + å¿«æ·é®æç€º
3.2 Slate.js çŒèŸåšæ žå¿é 眮
// editor/SlateEditor.tsx
import { createEditor, Descendant, Editor, Transforms } from 'slate';
import { Slate, Editable, withReact } from 'slate-react';
import { withHistory } from 'slate-history';
import { withChapters } from './plugins/withChapters';
import { withAutoSave } from './plugins/withAutoSave';
import { withWordCount } from './plugins/withWordCount';
import { withAITrace } from './plugins/withAITrace';
import { withMarkdownShortcuts } from './plugins/withMarkdownShortcuts';
// èªå®ä¹å
çŽ ç±»å
type CustomElement =
| { type: 'chapter-title'; level: number; children: CustomText[] }
| { type: 'paragraph'; children: CustomText[] }
| { type: 'scene-divider'; children: CustomText[] }
| { type: 'ai-highlight'; aiAction: string; traceId: string; children: CustomText[] };
type CustomText = {
text: string;
bold?: boolean;
italic?: boolean;
underline?: boolean;
aiGenerated?: boolean; // æ è®°AIçæçææ¬
aiTraceId?: string; // å
³èççID
};
export function WritingEditor({ workId, chapterId }: Props) {
const editor = useMemo(() =>
withMarkdownShortcuts(
withAITrace(
withWordCount(
withAutoSave(
withChapters(
withHistory(
withReact(createEditor())
)
), { interval: 30000 } // 30ç§èªåšä¿å
)
)
)
), []);
const [value, setValue] = useState<Descendant[]>(initialValue);
// å èœœç« èå
容
useEffect(() => {
loadChapterContent(workId, chapterId).then(content => {
setValue(content);
});
}, [workId, chapterId]);
return (
<Slate editor={editor} initialValue={value} onChange={setValue}>
<Toolbar />
<Editable
className="writing-area prose prose-lg mx-auto max-w-3xl
font-serif leading-relaxed tracking-wide
min-h-screen p-8 focus:outline-none"
placeholder="åŒå§åäœ..."
spellCheck={false}
renderElement={renderElement}
renderLeaf={renderLeaf}
onKeyDown={handleKeyDown}
/>
</Slate>
);
}
3.3 ç« è管çç³»ç»
| åèœ | å®ç° | 诎æ |
|---|---|---|
| ç« èå衚 | 巊䟧蟹æ ChapterSidebar | æŸç€ºå šéšç« è + åæ° + çŒèŸç¶æ |
| æ°å»ºç« è | ç¹å» + æ°ç« è | èªåšçŒå·ïŒç¬¬Nç« ïŒïŒå æ è·³å°æ é¢ |
| ç« è跳蜬 | ç¹å»å·ŠäŸ§ç« èå | å¹³æ»æ»åšå°å¯¹åºç« è |
| ç« èææœæåº | Drag & Drop | ææœè°æŽç« è顺åºïŒèªåšæŽæ°çŒå· |
| ç« èç¶æ | åŸæ æ è®° | â å·²å®ç» / âïžçŒèŸäž / ðèçš¿ |
| åæ°ç»è®¡ | 宿¶è®¡ç® | æ¯ç« åæ° + æ»åæ° + 仿¥æ°å¢ |
3.4 äžæ³šæš¡åŒ
ç¹å»åºéšç¶ææ ãäžæ³šæš¡åŒãæé®åïŒ
- éèå·ŠäŸ§ç« èæ + å³äŸ§AI颿¿
- çŒèŸåºå šå±å± äžïŒæŽå®œçåäœåºå
- èæ¯åæ¢äžºæè²äž»é¢ïŒæ€çŒïŒ
- å¿«æ·é®
Escéåºäžæ³šæš¡åŒ
åãPhase 1 · AIèŸ å©åäœ
4.1 AIè°çšæ¹åŒ
äœè æäžç§æ¹åŒè°çšAIïŒ
æ¹åŒäžïŒéäžææ¬ â AIæµ®åšèå
éäžäžæ®µæååïŒåŒ¹åºæµ®åšèåïŒ
| æäœ | åŸæ | åèœ | System Prompt æ žå¿ |
|---|---|---|---|
| æ©å | âš | å°éäžçæåæ©å±äžºæŽè¯Šç»çæå | å°ä»¥äžå 容æ©å䞺æŽè¯Šç»çåšçæåïŒä¿æåæé£æ Œå人称è§è§ãèŸåº1.5-2ååæ°ã |
| ç»å | âïž | ä»éäžäœçœ®ç»§ç»åäžå» | æ ¹æ®äžæç»§ç»åäœïŒä¿ææ èè¿èޝæ§åäººç©æ§æ ŒäžèŽæ§ãçæ200-500åã |
| æ¹å | ð | çšäžå衚蟟éåéäžå 容 | 以æŽäŒçŸ/æŽçާå/æŽæåŒ åçæ¹åŒéå以äžå 容ïŒä¿æåæäžåã |
| çµæ | ð¡ | åºäºåœåå 容ç»åºåç»æ è建议 | åºäºä»¥äžå 容ïŒç»åº3䞪äžåæ¹åçåç»æ èå»ºè®®ïŒæ¯äžª50åå ã |
| æèŠ | ð | 对éäžå å®¹çææèŠ | çšäžå¥è¯æŠæ¬ä»¥äžå å®¹çæ žå¿æ èã |
| é®ç¬çµ | â | å°éäžå 容åç»AIäŒäŒŽè®šè®º | 跳蜬å°å³äŸ§AI颿¿ïŒæºåžŠéäžå 容äœäžºäžäžæ |
æ¹åŒäºïŒå¿«æ·é® Ctrl+/ åŒAI
åšå
æ äœçœ®æ Ctrl+/ïŒåŒ¹åºåœä»€é¢æ¿ïŒèŸå
¥èªç±æä»€ïŒ
- ãåž®æåäžæ®µé𿝿åã
- ãè¿æ®µå¯¹è¯å€ªé¿ïŒçŒ©çäžåã
- ãç»å¥³äž»å äžå¥å å¿ç¬çœã
- ãæ£æ¥è¿äžç« ææ²¡æååççŸã
æ¹åŒäžïŒå³äŸ§AI婿颿¿
äžAIäŒäŒŽïŒç¬çµïŒèªç±å¯¹è¯ïŒè®šè®ºæ èã人ç©ãäžçè§çã对è¯å 容å¯äžé®æå ¥çŒèŸåšã
4.2 AIååºæµçš
çšæ·éäžææ¬ + ç¹å»ãæ©åã
â
å端åé请æ±:
{
action: "expand",
selectedText: "仿¬èµ·å€ŽïŒçå°å¥¹ç«åšéšå£ã",
context: {
chapterContent: "... åå500åäžäžæ ...",
chapterTitle: "第5ç« éé¢",
workTitle: "éžæ»çå°åšåŠ»",
genre: "éœåžèšæ
",
wordCountTarget: 3000
}
}
â
å端æå»ºå®æŽ prompt + è°AI
â
æµåŒè¿åç»æïŒSSE / WebSocketïŒ
â
å端åšçŒèŸåšäžæ¹æŸç€ºAI建议åºïŒ
âââââââââââââââââââââââââââââââââââââââ
â ð€ AIå»ºè®®ïŒæ©åïŒ â
â â
â 仿¬èµ·å€ŽïŒçå°å¥¹ç«åšéšå£ãç¯å
æåš â
â 她身äžïŒåäžå±æåçéç²ïŒåŸååºå¥¹ â
â 纀ç»ç蜮å»ã她埮埮䟧ç倎ïŒåäžä»è© â
â 倎æ»èœïŒé²åºäžæªçœççèé¢... â
â â
â [â
é纳] [â æç»] [ð éæ°çæ] â
âââââââââââââââââââââââââââââââââââââââ
â
çšæ·ç¹å» [â
é纳]
â
AIææ¬æå
¥çŒèŸåšïŒåžŠ aiGenerated=true æ è®°
â
åæ¶åå
¥ççè®°åœ
4.3 AIåäœ API
// POST /api/writing/ai/assist
Body: {
action: "expand" | "continue" | "rewrite" | "inspire" | "summarize" | "freeform",
selectedText: string, // éäžçææ¬
freeformPrompt?: string, // èªç±æä»€ïŒaction=freeformæ¶ïŒ
context: {
chapterId: string,
chapterContent: string, // åœåç« èå
šæïŒææªååå
500åïŒ
chapterTitle: string,
workTitle: string,
genre: string, // äœåç±»å
previousChapterSummary?: string // åäžç« æèŠ
}
}
Response (SSE æµåŒ): {
type: "chunk" | "done",
content: string, // æ¯æ¬¡è¿åäžåææ¬
traceId: string, // ççID
tokenUsage?: { // 宿æ¶è¿å
prompt: number,
completion: number
}
}
4.4 å端 AI åäœæå¡
// services/aiWritingService.ts
const WRITING_PROMPTS: Record<string, (ctx: any) => string> = {
expand: (ctx) => `äœ æ¯äžäœäžäžç${ctx.genre}å°è¯Žåäœå©æã
äœåïŒã${ctx.workTitle}ãïŒåœåç« èïŒ${ctx.chapterTitle}ã
请å°ä»¥äžå
容æ©å䞺æŽè¯Šç»çåšçæåïŒä¿æåæé£æ Œå人称è§è§ã
æ©åå°åæç1.5-2ååæ°ãåªèŸåºæ©ååçææ¬ïŒäžèŠå ä»»äœè§£éã
äžäžæïŒ
${ctx.chapterContent}
éèŠæ©åçå
容ïŒ
${ctx.selectedText}`,
continue: (ctx) => `äœ æ¯äžäœäžäžç${ctx.genre}å°è¯Žåäœå©æã
äœåïŒã${ctx.workTitle}ãïŒåœåç« èïŒ${ctx.chapterTitle}ã
è¯·æ ¹æ®ä»¥äžå
容继ç»åäœïŒä¿ææ
èè¿èޝæ§åäººç©æ§æ ŒäžèŽæ§ã
çæ200-500åãåªèŸåºç»åçææ¬ã
åœåå
容ïŒ
${ctx.chapterContent}
ä»è¿éç»§ç»ïŒ
${ctx.selectedText}`,
rewrite: (ctx) => `äœ æ¯äžäœäžäžç${ctx.genre}å°è¯Žåäœå©æã
äœåïŒã${ctx.workTitle}ãïŒåœåç« èïŒ${ctx.chapterTitle}ã
请以æŽäŒçŸç衚蟟éå以äžå
容ïŒä¿æåæäžåïŒåæ°çžè¿ã
åªèŸåºéååçææ¬ã
åæïŒ
${ctx.selectedText}`,
inspire: (ctx) => `äœ æ¯äžäœäžäžç${ctx.genre}å°è¯Žåäœå©æã
äœåïŒã${ctx.workTitle}ãïŒåœåç« èïŒ${ctx.chapterTitle}ã
åºäºä»¥äžå
容ïŒç»åº3䞪äžåæ¹åçåç»æ
èå»ºè®®ïŒæ¯äžª50åå
ã
æ ŒåŒïŒ1. ⊠2. ⊠3. âŠ
åœåå
容ïŒ
${ctx.selectedText}`,
summarize: (ctx) => `çšäžå¥è¯æŠæ¬ä»¥äžå
å®¹çæ žå¿æ
èïŒ
${ctx.selectedText}`,
freeform: (ctx) => `äœ æ¯äžäœäžäžç${ctx.genre}å°è¯Žåäœå©æã
äœåïŒã${ctx.workTitle}ãïŒåœåç« èïŒ${ctx.chapterTitle}ã
åœåå
容ïŒ
${ctx.chapterContent}
äœè
çæä»€ïŒ
${ctx.freeformPrompt}
çžå
³å
容ïŒ
${ctx.selectedText}`
};
export async function* streamAIWriting(params: AIWritingParams) {
const prompt = WRITING_PROMPTS[params.action](params.context);
const stream = await openai.chat.completions.create({
model: 'gpt-4',
messages: [
{ role: 'system', content: prompt },
{ role: 'user', content: params.selectedText || params.freeformPrompt }
],
temperature: 0.8, // åäœç±»ä»»å¡ç¥é«æž©åºŠ
max_tokens: 1500,
stream: true
});
let fullContent = '';
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
fullContent += content;
yield { type: 'chunk', content };
}
yield {
type: 'done',
content: fullContent,
tokenUsage: { prompt: 0, completion: 0 } // ä» stream äžæå
};
}
äºãPhase 2 · å šçšççç³»ç»
5.1 æ žå¿åå
5.2 ççæ°æ®ç»æ
interface AITrace {
traceId: string; // å¯äžID
workId: string; // äœåID
chapterId: string; // ç« èID
userId: string; // äœè
ID
timestamp: string; // ISOæ¶éŽæ³
action: 'expand' | 'continue' | 'rewrite' | 'inspire' | 'summarize' | 'freeform';
input: {
selectedText: string; // åæïŒéäžçæåïŒ
prompt?: string; // èªç±æä»€ïŒåŠæïŒ
};
output: {
generatedText: string; // AIçæçæå
tokenUsage: {
prompt: number;
completion: number;
};
};
decision: 'accepted' | 'rejected' | 'modified' | 'pending';
finalText?: string; // åŠæäœè
ä¿®æ¹äºAI建议åé纳ïŒè®°åœæç»ææ¬
wordDelta: number; // åæ°ååïŒ+120 衚瀺AI莡ç®äº120åïŒ
}
5.3 ççæ¶æº
| äºä»¶ | è®°åœå 容 | decision ç¶æ |
|---|---|---|
| çšæ·è°çšAI | è®°åœ input + output | pending |
| çšæ·ç¹å» é纳 | æŽæ° decision | accepted |
| çšæ·ç¹å» æç» | æŽæ° decision | rejected |
| çšæ·ä¿®æ¹åé纳 | æŽæ° decision + finalText | modified |
| çšæ·å ³é建议åºïŒæªæäœïŒ | æŽæ° decision | rejected |
5.4 ççæŸç€º
å³äŸ§AI颿¿çãççè®°åœãåºåïŒææ¶éŽååºæŸç€ºïŒ
ççè®°åœ (12)
â 21:05 âšæ©å +120å â
é纳
â 21:03 âïžç»å +85å â
é纳
â 20:58 ðæ¹å 38å âæç»
â 20:55 ð¡çµæ 3æ¡ â
é纳#2
â 20:50 âšæ©å +200å âïžä¿®æ¹åé纳
â ...
ç¹å»æ¯æ¡è®°åœå¯å±åŒæ¥ç诊æ ïŒåæ â AIçæ â æç»é纳ïŒã
5.5 API
// è®°åœçç
POST /api/writing/traces
Body: { ...AITrace }
// è·åç« èççå衚
GET /api/writing/traces?workId=xxx&chapterId=xxx
Response: { traces: AITrace[] }
// æŽæ°ççç¶æïŒé纳/æç»ïŒ
PATCH /api/writing/traces/:traceId
Body: { decision: "accepted" | "rejected" | "modified", finalText?: string }
5.6 Notion æ°æ®åº â AIçç衚
| åæ®µå | ç±»å | 诎æ |
|---|---|---|
| ççID | title | å¯äžæ è¯ |
| äœåID | text | å ³èäœå |
| ç« èID | text | å ³èç« è |
| äœè ID | text | å ³èçšæ· |
| AIæäœç±»å | select | expand / continue / rewrite / inspire / summarize / freeform |
| åæ | text | éäžçæå |
| AIçæææ¬ | text | AIèŸåº |
| æç»ææ¬ | text | äœè ä¿®æ¹åçææ¬ïŒåŠæïŒ |
| å³ç | select | accepted / rejected / modified / pending |
| åæ°åå | number | AI莡ç®çåæ°åå |
| Tokenæ¶è | number | prompt + completion tokens |
| æäœæ¶éŽ | date | æ¶éŽæ³ |
å ãPhase 3 · AIäœ¿çšæ¥åïŒABCD级ïŒ
6.1 æ¥åç级å®ä¹
| ç级 | åAIç | å«ä¹ | æ çŸé¢è² | 诎æ |
|---|---|---|---|---|
| A | â€10% | å ä¹å šéšåå | ð¢ ç»¿è² | AIä» æäŸå°éçµææåŸ®è°ïŒåæå®å šåœäœè |
| B | 10%-30% | AIèŸ å©åäœ | ð¡ é»è² | AIæäŸäºéšåæ©åæèŸ å©ïŒäœæ žå¿åæåæ¶æåœäœè |
| C | 30%-50% | AI深床åäž | ð æ©è² | AIåäžäºå€§éåäœïŒäœäœè è¿è¡äºå®èŽšæ§ä¿®æ¹åçŒèŸ |
| D | >50% | AI䞻富åäœ | ðŽ çº¢è² | AIçæäºå€§éšåå 容ïŒäœè 莡ç®èŸå° |
6.2 åAIç计ç®å ¬åŒ
// utils/reportGenerator.ts
interface AIReportData {
totalWords: number; // äœåæ»åæ°
aiAcceptedWords: number; // AIçæäžè¢«é纳çåæ°
aiModifiedWords: number; // AIçæäœè¢«ä¿®æ¹åé纳çåæ°
humanOnlyWords: number; // 纯人类åäœçåæ°
traces: AITrace[]; // å
šéšççè®°åœ
}
function calculateRawAIRate(data: AIReportData): number {
// åAIç = AIéçº³åæ° / æ»åæ°
// âä¿®æ¹åé纳âæ50%计å
¥AIåæ°ïŒäœè
è¿è¡äºä¿®æ¹ïŒç®å䞪ååïŒ
const effectiveAIWords = data.aiAcceptedWords + (data.aiModifiedWords * 0.5);
return Math.round((effectiveAIWords / data.totalWords) * 100 * 10) / 10;
}
function getGrade(rawAIRate: number): 'A' | 'B' | 'C' | 'D' {
if (rawAIRate <= 10) return 'A';
if (rawAIRate <= 30) return 'B';
if (rawAIRate <= 50) return 'C';
return 'D';
}
function generateReport(data: AIReportData): AIReport {
const rawAIRate = calculateRawAIRate(data);
const grade = getGrade(rawAIRate);
const traceStats = {
total: data.traces.length,
accepted: data.traces.filter(t => t.decision === 'accepted').length,
rejected: data.traces.filter(t => t.decision === 'rejected').length,
modified: data.traces.filter(t => t.decision === 'modified').length,
byAction: {
expand: data.traces.filter(t => t.action === 'expand').length,
continue: data.traces.filter(t => t.action === 'continue').length,
rewrite: data.traces.filter(t => t.action === 'rewrite').length,
inspire: data.traces.filter(t => t.action === 'inspire').length,
freeform: data.traces.filter(t => t.action === 'freeform').length,
}
};
return {
workId: data.workId,
grade,
rawAIRate,
totalWords: data.totalWords,
aiContributionWords: data.aiAcceptedWords + data.aiModifiedWords,
humanWords: data.humanOnlyWords,
acceptanceRate: Math.round((traceStats.accepted + traceStats.modified) / traceStats.total * 100),
traceStats,
generatedAt: new Date().toISOString(),
// æ¥åæèŠïŒçšAIçæäžå¥è¯æè¿°ïŒ
summary: `æ¬äœå${grade}级ïŒåAIç${rawAIRate}%ãå
±${data.totalWords}åïŒAIåäž${traceStats.total}次ïŒé纳ç${traceStats.accepted + traceStats.modified}/${traceStats.total}ã`
};
}
6.3 æ¥åå±ç€ºçé¢
âââââââââââââââââââââââââââââââââââââââââââââââ
â AIäœ¿çšæ¥å · ãéžæ»çå°åšåŠ»ã â
âââââââââââââââââââââââââââââââââââââââââââââââ€
â â
â âââââââââââââââ â
â â â â
â â ð¢ A â â
â â 8.2% â â
â â â â
â âââââââââââââââ â
â å ä¹å
šéšåå â
â â
âââââââââââââââââââââââââââââââââââââââââââââââ€
â æ»åæ° 45,230å â
â 人类åå 41,520å (91.8%) â
â AIèŽ¡ç® 3,710å (8.2%) â
âââââââââââââââââââââââââââââââââââââââââââââââ€
â AIæäœç»è®¡ â
â ⢠æ»è°çš: 47次 â
â ⢠æ©å: 18次 ç»å: 12次 æ¹å: 8次 â
â â¢ çµæ: 6次 èªç±æä»€: 3次 â
â ⢠é纳ç: 75% (35/47) â
â ⢠修æ¹åé纳: 8次 â
âââââââââââââââââââââââââââââââââââââââââââââââ€
â ç« èæç» â
â 第1ç« : A(3.2%) 第2ç« : A(5.1%) â
â 第3ç« : B(12.4%) 第4ç« : A(7.8%) â
â 第5ç« : A(9.0%) ... â
âââââââââââââââââââââââââââââââââââââââââââââââ€
â æ¥åçææ¶éŽ: 2026-03-25 21:30 â
â æ¬æ¥åç±ç³»ç»èªåšçæïŒäœè
äžå¯ä¿®æ¹ã â
âââââââââââââââââââââââââââââââââââââââââââââââ
6.4 æ¥å API
// çæ/æŽæ°äœåæ¥å
POST /api/writing/reports/generate
Body: { workId: string }
Response: { report: AIReport }
// è·åäœåæ¥å
GET /api/writing/reports/:workId
Response: { report: AIReport }
// è·åç« èçº§å«æ¥å
GET /api/writing/reports/:workId/chapters
Response: { chapters: ChapterReport[] }
äžãäœå管ç
7.1 äœåå衚页
çšæ·ä» Dashboard ç¹å»ãåŒå§åäœãè¿å ¥äœåå衚ïŒ
âââââââââââââââââââââââââââââââââââââââââââââââ
â ð æçäœå [+ æ°å»ºäœå] â
âââââââââââââââââââââââââââââââââââââââââââââââ€
â â
â âââââââââââââââââââââââââââââââââââââââ â
â â ãéžæ»çå°åšåŠ»ã ð¢ A (8.2%) â â
â â éœåžèšæ
· 20ç« Â· 45,230å · è¿èœœäž â â
â â æåçŒèŸ: 10åéå â â
â âââââââââââââââââââââââââââââââââââââââ â
â â
â âââââââââââââââââââââââââââââââââââââââ â
â â ãæèŸ°åã ð¡ B (15.6%) â â
â â ä»äŸ å¥å¹» · 8ç« Â· 18,400å · èçš¿ â â
â â æåçŒèŸ: 2倩å â â
â âââââââââââââââââââââââââââââââââââââââ â
â â
âââââââââââââââââââââââââââââââââââââââââââââââ
7.2 æ°å»ºäœåæµçš
çšæ·ç¹å» [+ æ°å»ºäœå]
â
匹åºè¡šåïŒ
- äœååç§°ïŒå¿
å¡«ïŒ
- äœåç±»åïŒéæ©ïŒïŒéœåžèšæ
/ ä»äŸ å¥å¹» / æ¬çæšç / ç§å¹» / åå² / å
¶ä»
- ç®ä»ïŒéå¡«ïŒ
â
å建æå â èªåšè¿å
¥çŒèŸåš â èªåšå建ã第1ç« ã
7.3 Notion æ°æ®åº â äœå衚
| åæ®µå | ç±»å | 诎æ |
|---|---|---|
| äœåID | title | å¯äžæ è¯ |
| äœååç§° | text | äœåæ é¢ |
| äœè ID | text | å ³èçšæ· |
| äœåç±»å | select | éœåžèšæ / ä»äŸ å¥å¹» / æ¬çæšç / ç§å¹» / åå² / å ¶ä» |
| æ»åæ° | number | 宿¶æŽæ° |
| ç« èæ° | number | 宿¶æŽæ° |
| AIæ¥åç级 | select | A / B / C / D |
| åAIç | number | çŸåæ¯ |
| ç¶æ | select | èçš¿ / è¿èœœäž / å·²å®ç» / å·²ååž |
| ç®ä» | text | äœåç®ä» |
| å建æ¶éŽ | date | å建æ¶éŽ |
| æåçŒèŸ | date | æåçŒèŸæ¶éŽ |
7.4 Notion æ°æ®åº â ç« è衚
| åæ®µå | ç±»å | 诎æ |
|---|---|---|
| ç« èID | title | å¯äžæ è¯ |
| äœåID | text | å ³èäœå |
| ç« èæ é¢ | text | åŠã第1ç« çžéã |
| ç« èåºå· | number | æåºçš |
| å 容 | text | Slate JSON åºååååš |
| åæ° | number | æ¬ç« åæ° |
| ç¶æ | select | èçš¿ / çŒèŸäž / å·²å®ç» |
| æåçŒèŸ | date | æåä¿åæ¶éŽ |
å «ãAPI æ»è§
| æ¥å£ | æ¹æ³ | åèœ | éŽæ |
|---|---|---|---|
/api/writing/works |
GET | è·åæçäœåå衚 | JWT |
/api/writing/works |
POST | æ°å»ºäœå | JWT |
/api/writing/works/:id |
GET | è·åäœå诊æ | JWT |
/api/writing/works/:id |
PATCH | æŽæ°äœåä¿¡æ¯ | JWT |
/api/writing/works/:id/chapters |
GET | è·åç« èå衚 | JWT |
/api/writing/works/:id/chapters |
POST | æ°å»ºç« è | JWT |
/api/writing/chapters/:id |
GET | è·åç« èå 容 | JWT |
/api/writing/chapters/:id |
PUT | ä¿åç« èå 容 | JWT |
/api/writing/chapters/:id/order |
PATCH | è°æŽç« èé¡ºåº | JWT |
/api/writing/ai/assist |
POST | AIèŸ å©åäœïŒSSEæµïŒ | JWT |
/api/writing/traces |
POST | è®°åœçç | JWT |
/api/writing/traces |
GET | è·åççå衚 | JWT |
/api/writing/traces/:id |
PATCH | æŽæ°ççç¶æ | JWT |
/api/writing/reports/:workId |
GET | è·åAIæ¥å | JWT |
/api/writing/reports/generate |
POST | çæ/æŽæ°æ¥å | JWT |
ä¹ãè·¯ç±è§å
| è·¯ç± | é¡µé¢ | 诎æ |
|---|---|---|
/writing/works |
WorkListPage | äœåå衚 |
/writing/editor/:workId |
EditorPage | ç åå°ïŒé»è®€æåŒæåçŒèŸç« èïŒ |
/writing/editor/:workId/:chapterId |
EditorPage | ç åå°ïŒæå®ç« èïŒ |
/writing/report/:workId |
AIReportPage | AIäœ¿çšæ¥å诊æ |
åãå¿«æ·é®
| å¿«æ·é® | åèœ |
|---|---|
Ctrl+S |
æåšä¿å |
Ctrl+/ |
åŒAI婿ïŒèªç±æä»€ïŒ |
Ctrl+Shift+E |
æ©åéäžææ¬ |
Ctrl+Shift+W |
ç»å |
Ctrl+Shift+R |
æ¹åéäžææ¬ |
Ctrl+Z |
æ€é |
Ctrl+Shift+Z |
éå |
Esc |
éåºäžæ³šæš¡åŒ / å ³éAIå»ºè®®åº |
F11 |
åæ¢äžæ³šæš¡åŒ |
åäžãéªæ¶æ å
| éªæ¶é¡¹ | æ å |
|---|---|
| çŒèŸåšå¯çš | 坿µç èŸå ¥äžæïŒæ ŒåŒåæ£åžžïŒæ å¡é¡¿ |
| ç« è管ç | æ°å»º/忢/ææœæåºæ£åžžïŒåæ°å®æ¶ç»è®¡ |
| èªåšä¿å | 30ç§èªåšä¿å + 倱çŠä¿åïŒåºéšç¶ææ æŸç€ºä¿åæ¶éŽ |
| AIæµ®åšèå | éäžææ¬å匹åº6䞪AIæäœæé® |
| AIæµåŒååº | AIç»ææµåŒæŸç€ºïŒSSEïŒïŒäžæ¯äžæ¬¡æ§å 蜜 |
| é纳/æç» | é纳åAIææ¬æå ¥çŒèŸåšïŒæç»åæž é€å»ºè®®åº |
| ççè®°åœ | æ¯æ¬¡AIæäœèªåšççïŒå³äŸ§é¢æ¿å¯æ¥ç |
| AIæ¥å | å¯çæABCD级æ¥åïŒççº§åæ°æ®æ£ç¡® |
| äžæ³šæš¡åŒ | 忢åå šå±åäœïŒæè²äž»é¢ïŒEscéåº |
| ç§»åšç«¯ | ææºç«¯å¯çŒèŸïŒå·Šå³äŸ§æ èªåšæ¶èµ·ïŒ |
| æ°æ®æä¹ å | äœå/ç« è/ççå šéšåå ¥ Notion æ°æ®åº |
åäºãæ§è¡æ¶åº
Phase 0: Slate.jsçŒèŸåš + ç« è管ç + å·¥å
·æ + èªåšä¿åïŒé¢è®¡ 4hïŒ
â
Phase 1: AIèŸ
å©åäœïŒæµ®åšèå + SSEæµ + 6ç§Action + å³äŸ§AI颿¿ïŒïŒé¢è®¡ 3hïŒ
â
Phase 2: å
šçšççç³»ç»ïŒèªåšè®°åœ + ççæŸç€º + NotionååšïŒïŒé¢è®¡ 2hïŒ
â
Phase 3: AIäœ¿çšæ¥åïŒABCDè®¡ç® + æ¥åçé¢ + äœåå衚æŸç€ºïŒïŒé¢è®¡ 2hïŒ
â
Phase 4: äœå管çïŒå衚页 + æ°å»ºäœå + CRUD APIïŒïŒé¢è®¡ 1.5hïŒ
â
Phase 5: åå端èè° + Nginxè·¯ç± + éšçœ²ïŒé¢è®¡ 1.5hïŒ
â
â é¢è§ç«æµè¯ â äººç±»éªæ¶ â æ£åŒç«äžçº¿
åäžãåç»æ©å±é¢ç
æ¬æå®æåïŒåç»å¯æ©å±ïŒäžå𿬿什èåŽïŒïŒ
- 富åºåèœ â 富åºäžº TXT / DOCX / PDF
- åäœçŒèŸ â å€äººå®æ¶åäœçŒèŸïŒCRDTïŒ
- çæ¬åå² â æ¯æ¬¡ä¿åç宿Žåå²è®°åœ
- æš¡æ¿åº â çœæåç±»ååŒå€Ž/倧纲暡æ¿
- ç« èç»è®¡ â åäœæ¶éŽæ²çº¿ãåæ°è¶å¿åŸ
- å€ç«¯åæ¥ â çµè/ææº/å¹³æ¿æ çŒåæ¢