Skip to content

Commit 77064a4

Browse files
committed
feat(sop): SOP 系统 MVP——四表落库/模板运行状态机/三页面/5 条 lint 规则
1 parent 9e625ca commit 77064a4

10 files changed

Lines changed: 1513 additions & 1 deletion

File tree

‎client/electron/db/dbIpcHandlers.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,20 @@ const ALLOWED_TABLES = new Set([
4444
'implementation_intentions',
4545
// imports — 知识入籍记录(v8 迁移新增)
4646
'imports',
47+
// sop_templates — SOP 标准作业流程模板(v9 迁移新增)
48+
'sop_templates',
49+
// sop_steps — SOP 模板步骤(v9 迁移新增)
50+
'sop_steps',
51+
// sop_runs — SOP 执行记录(v9 迁移新增)
52+
'sop_runs',
53+
// inbox_items — 统一收件箱(v9 迁移新增)
54+
'inbox_items',
55+
// beta_profile — 内测身份本地缓存(v10 新增,临时收入方案)
56+
'beta_profile',
57+
// licenses — 激活码本地缓存(v10 新增)
58+
'licenses',
59+
// invite_codes — 邀请码本地缓存(v10 新增)
60+
'invite_codes',
4761
]);
4862

4963
/** camelCase → snake_case(用于表名映射) */
@@ -83,6 +97,15 @@ const TABLE_NAME_MAP: Record<string, string> = {
8397
worldSnapshots: 'world_snapshots',
8498
// v8 新增:知识入籍记录(阶段 A 入口问题)
8599
imports: 'imports',
100+
// v9 新增:SOP 标准作业流程 + 统一收件箱(Wave 0 地基)
101+
sopTemplates: 'sop_templates',
102+
sopSteps: 'sop_steps',
103+
sopRuns: 'sop_runs',
104+
inboxItems: 'inbox_items',
105+
// v10 新增:内测身份/激活码/邀请码(临时收入方案)
106+
betaProfile: 'beta_profile',
107+
licenses: 'licenses',
108+
inviteCodes: 'invite_codes',
86109
};
87110

88111
function resolveTable(table: string): string {

‎client/electron/db/schema.ts‎

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import type Database from 'better-sqlite3';
88
import { logger } from '../logger.js';
99

10-
export const SCHEMA_VERSION = 8;
10+
export const SCHEMA_VERSION = 10;
1111

1212
export const SCHEMA_DDL = /* sql */ `
1313
CREATE TABLE IF NOT EXISTS pomodoro_sessions (
@@ -204,6 +204,82 @@ CREATE TABLE IF NOT EXISTS imports (
204204
settled_at TEXT NOT NULL
205205
);
206206
CREATE INDEX IF NOT EXISTS idx_imports_settled_at ON imports(settled_at);
207+
-- v9:SOP 标准作业流程模板/步骤/执行记录 + 统一收件箱(Wave 0 地基)
208+
CREATE TABLE IF NOT EXISTS sop_templates (
209+
id TEXT PRIMARY KEY,
210+
name TEXT NOT NULL,
211+
description TEXT NOT NULL DEFAULT '',
212+
icon TEXT NOT NULL DEFAULT '',
213+
category TEXT NOT NULL DEFAULT '',
214+
source TEXT NOT NULL DEFAULT 'user' CHECK (source IN ('builtin','user')),
215+
created_at TEXT NOT NULL,
216+
updated_at TEXT NOT NULL
217+
);
218+
CREATE TABLE IF NOT EXISTS sop_steps (
219+
id TEXT PRIMARY KEY,
220+
template_id TEXT NOT NULL,
221+
step_type TEXT NOT NULL,
222+
title TEXT NOT NULL DEFAULT '',
223+
config TEXT NOT NULL DEFAULT '{}',
224+
"order" INTEGER NOT NULL DEFAULT 0,
225+
FOREIGN KEY (template_id) REFERENCES sop_templates(id) ON DELETE CASCADE
226+
);
227+
CREATE TABLE IF NOT EXISTS sop_runs (
228+
id TEXT PRIMARY KEY,
229+
template_id TEXT NOT NULL,
230+
status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running','awaiting_module','completed','aborted')),
231+
current_step_index INTEGER NOT NULL DEFAULT 0,
232+
step_progress TEXT NOT NULL DEFAULT '{}',
233+
started_at TEXT NOT NULL,
234+
finished_at TEXT,
235+
FOREIGN KEY (template_id) REFERENCES sop_templates(id) ON DELETE CASCADE
236+
);
237+
CREATE TABLE IF NOT EXISTS inbox_items (
238+
id TEXT PRIMARY KEY,
239+
source TEXT NOT NULL DEFAULT 'clipboard' CHECK (source IN ('clipboard','inspiration','import')),
240+
title TEXT NOT NULL DEFAULT '',
241+
content TEXT NOT NULL DEFAULT '',
242+
status TEXT NOT NULL DEFAULT 'new' CHECK (status IN ('new','settled','archived')),
243+
created_at TEXT NOT NULL
244+
);
245+
CREATE INDEX IF NOT EXISTS idx_sop_steps_template_order ON sop_steps(template_id,"order");
246+
CREATE INDEX IF NOT EXISTS idx_sop_runs_status_started ON sop_runs(status,started_at);
247+
CREATE INDEX IF NOT EXISTS idx_inbox_items_status_created ON inbox_items(status,created_at DESC);
248+
-- v10:内测身份/激活码/邀请码表(临时收入方案)
249+
CREATE TABLE IF NOT EXISTS beta_profile (
250+
id TEXT PRIMARY KEY,
251+
user_id TEXT NOT NULL,
252+
tier TEXT NOT NULL DEFAULT 'observer',
253+
cohort INTEGER NOT NULL DEFAULT 1,
254+
joined_at TEXT NOT NULL,
255+
lifetime_pro INTEGER NOT NULL DEFAULT 0,
256+
badges TEXT NOT NULL DEFAULT '[]',
257+
perks_config TEXT NOT NULL DEFAULT '{}',
258+
synced_at TEXT
259+
);
260+
CREATE TABLE IF NOT EXISTS licenses (
261+
id TEXT PRIMARY KEY,
262+
code TEXT NOT NULL UNIQUE,
263+
type TEXT NOT NULL,
264+
tier TEXT NOT NULL,
265+
status TEXT NOT NULL DEFAULT 'active',
266+
machine_id TEXT,
267+
activated_at TEXT,
268+
expires_at TEXT,
269+
synced_at TEXT
270+
);
271+
CREATE TABLE IF NOT EXISTS invite_codes (
272+
id TEXT PRIMARY KEY,
273+
code TEXT NOT NULL UNIQUE,
274+
issuer_user_id TEXT NOT NULL,
275+
status TEXT NOT NULL DEFAULT 'pending',
276+
used_by_user_id TEXT,
277+
used_at TEXT,
278+
created_at TEXT NOT NULL
279+
);
280+
CREATE INDEX IF NOT EXISTS idx_beta_profile_user_id ON beta_profile(user_id);
281+
CREATE INDEX IF NOT EXISTS idx_licenses_code ON licenses(code);
282+
CREATE INDEX IF NOT EXISTS idx_invite_codes_issuer ON invite_codes(issuer_user_id);
207283
`;
208284

209285
/** v3 迁移 DDL:CRDT 同步引擎元数据表(条件执行) */
@@ -275,6 +351,9 @@ export function initializeSchema(db: Database.Database): void {
275351
}
276352
}
277353

354+
// v9 迁移:SOP 模板/步骤/执行记录 + 统一收件箱(sop_templates/sop_steps/sop_runs/inbox_items)
355+
// 表 DDL 已包含在 SCHEMA_DDL 中(CREATE IF NOT EXISTS 幂等),此处无需额外操作
356+
278357
db.pragma(`user_version = ${SCHEMA_VERSION}`);
279358
}
280359

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* SOP 内置模板种子 — 首次启动幂等写入
3+
* Built-in SOP template seeds — idempotently written on first launch
4+
*
5+
* @ai-context: source='builtin' 的只读模板。ensureBuiltinTemplates 按 id
6+
* 检测缺失后批量写入(含 steps),重复调用不产生重复数据;builtin 模板
7+
* 在 UI 上禁止编辑/删除(仅可复制为 user 模板)。
8+
* @ai-context: Read-only builtin templates; seeding is idempotent by id.
9+
* Builtins are immutable in UI (copy-to-user instead of edit/delete).
10+
*/
11+
import type { SopStepType } from '../types';
12+
13+
/** 内置模板种子定义(steps 用宽松结构,写入时补 id/order) */
14+
export interface BuiltinTemplateSeed {
15+
id: string;
16+
name: string;
17+
description: string;
18+
icon: string;
19+
category: string;
20+
steps: Array<{
21+
step_type: SopStepType;
22+
title: string;
23+
durationMinutes?: number;
24+
target?: string;
25+
module?: string;
26+
}>;
27+
}
28+
29+
/** 2-3 个内置模板:费曼讲解 / 番茄工作法 / 错题复盘 */
30+
export const BUILTIN_TEMPLATES: BuiltinTemplateSeed[] = [
31+
{
32+
id: 'sop-feynman-five',
33+
name: '费曼讲解五步',
34+
description: '把概念讲到别人能听懂——讲解、找缺口、补、再讲',
35+
icon: '🧠',
36+
category: '费曼学习法',
37+
steps: [
38+
{ step_type: 'focus', title: '选择概念并写目标', durationMinutes: 3 },
39+
{ step_type: 'output', title: '完整讲解一遍(录音)', durationMinutes: 15 },
40+
{ step_type: 'review', title: '回放找出薄弱点', durationMinutes: 5 },
41+
{ step_type: 'focus', title: '回炉补缺(查资料/重读)', durationMinutes: 10 },
42+
{ step_type: 'output', title: '简化重讲直到无缺口', durationMinutes: 10 },
43+
],
44+
},
45+
{
46+
id: 'sop-pomodoro-cycle',
47+
name: '番茄工作法',
48+
description: '25 分钟专注 + 5 分钟休息,四轮后长休',
49+
icon: '🍅',
50+
category: '专注节奏',
51+
steps: [
52+
{ step_type: 'focus', title: '专注第 1 轮', durationMinutes: 25 },
53+
{ step_type: 'break', title: '休息 5 分钟', durationMinutes: 5 },
54+
{ step_type: 'focus', title: '专注第 2 轮', durationMinutes: 25 },
55+
{ step_type: 'break', title: '休息 5 分钟', durationMinutes: 5 },
56+
{ step_type: 'focus', title: '专注第 3 轮', durationMinutes: 25 },
57+
{ step_type: 'break', title: '休息 5 分钟', durationMinutes: 5 },
58+
{ step_type: 'focus', title: '专注第 4 轮', durationMinutes: 25 },
59+
{ step_type: 'break', title: '长休息 15 分钟', durationMinutes: 15 },
60+
],
61+
},
62+
{
63+
id: 'sop-error-replay',
64+
name: '错题复盘',
65+
description: '错题 → 重做 → 提炼规则 → 沉淀为闪卡',
66+
icon: '🎯',
67+
category: '错题管理',
68+
steps: [
69+
{ step_type: 'review', title: '回顾错题与当时的思路', durationMinutes: 5 },
70+
{ step_type: 'output', title: '不看答案重做一遍', durationMinutes: 10 },
71+
{ step_type: 'focus', title: '对比答案,提炼错误规则', durationMinutes: 8 },
72+
{ step_type: 'module', title: '生成闪卡到复习队列', durationMinutes: 5, target: '/flashcards', module: '闪卡' },
73+
],
74+
},
75+
];
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* SOP 模板 lint 规则 — 编辑器实时提示(纯函数,无副作用)
3+
* SOP template lint rules — pure-function editor feedback
4+
*
5+
* @ai-context: 5 条规则:no-break-long(休息过长)/ no-review(无回顾步骤)/
6+
* too-many-steps(步骤过多)/ no-output(无产出步骤)/ short-total(总时长过短)。
7+
* 每条规则返回 { rule, severity, message };severity 区分 warn(建议)与
8+
* error(必须修正),编辑器据此展示黄/红提示。
9+
* @ai-context: Five rules checked against a draft template; severity drives
10+
* warning vs error styling in the editor.
11+
*/
12+
import type { SopTemplate } from '../types';
13+
import { templateTotalMinutes } from '../types';
14+
15+
export interface SopLintIssue {
16+
/** 规则标识,前端据此去重与渲染图标 */
17+
rule: 'no-break-long' | 'no-review' | 'too-many-steps' | 'no-output' | 'short-total';
18+
severity: 'warn' | 'error';
19+
message: string;
20+
}
21+
22+
/** 休息步骤单步时长上限(分钟)——超过视为"长休息"风险 */
23+
const MAX_BREAK_MINUTES = 30;
24+
25+
/** 步骤总数上限——超过视为过载流程 */
26+
const MAX_STEPS = 12;
27+
28+
/** 总时长下限(分钟)——低于视为不足以形成有效流程 */
29+
const MIN_TOTAL_MINUTES = 10;
30+
31+
/**
32+
* 对模板草稿执行全部 lint 规则。
33+
* @param draft 编辑器中的未持久化模板(steps 为原始行,config 可为 '{}')
34+
* @returns 问题列表(空数组 = 通过)
35+
*/
36+
export function lintSopTemplate(draft: Pick<SopTemplate, 'steps'>): SopLintIssue[] {
37+
const issues: SopLintIssue[] = [];
38+
const steps = draft.steps;
39+
40+
if (steps.length === 0) {
41+
issues.push({
42+
rule: 'too-many-steps',
43+
severity: 'error',
44+
message: '流程还没有任何步骤——请至少添加一个步骤',
45+
});
46+
return issues;
47+
}
48+
49+
// no-break-long:任一休息步骤超过上限
50+
const longBreak = steps.find((s) => s.step_type === 'break' && (s.configParsed.durationMinutes ?? 0) > MAX_BREAK_MINUTES);
51+
if (longBreak) {
52+
issues.push({
53+
rule: 'no-break-long',
54+
severity: 'warn',
55+
message: `「${longBreak.title}」休息 ${longBreak.configParsed.durationMinutes} 分钟过长,建议 ≤${MAX_BREAK_MINUTES} 分钟`,
56+
});
57+
}
58+
59+
// no-review:流程无回顾/复习步骤(跨模块跳转复习不计入)
60+
if (!steps.some((s) => s.step_type === 'review')) {
61+
issues.push({
62+
rule: 'no-review',
63+
severity: 'warn',
64+
message: '流程中没有回顾(review)步骤——缺少巩固环节,建议添加',
65+
});
66+
}
67+
68+
// too-many-steps:步骤过多
69+
if (steps.length > MAX_STEPS) {
70+
issues.push({
71+
rule: 'too-many-steps',
72+
severity: 'warn',
73+
message: `步骤数 ${steps.length} 超过 ${MAX_STEPS},长流程难以坚持,建议拆分`,
74+
});
75+
}
76+
77+
// no-output:无产出步骤(focus 含产出物描述不视为 output)
78+
if (!steps.some((s) => s.step_type === 'output')) {
79+
issues.push({
80+
rule: 'no-output',
81+
severity: 'warn',
82+
message: '流程中没有产出(output)步骤——结束时缺少可见成果,建议添加',
83+
});
84+
}
85+
86+
// short-total:总时长过短
87+
const total = templateTotalMinutes({ steps });
88+
if (total > 0 && total < MIN_TOTAL_MINUTES) {
89+
issues.push({
90+
rule: 'short-total',
91+
severity: 'warn',
92+
message: `总时长仅 ${total} 分钟,低于 ${MIN_TOTAL_MINUTES} 分钟,建议延长`,
93+
});
94+
}
95+
96+
return issues;
97+
}

0 commit comments

Comments
 (0)