codebee 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +392 -0
- package/app/__init__.py +0 -0
- package/app/core/__init__.py +0 -0
- package/app/core/attachments.py +322 -0
- package/app/core/automation.py +585 -0
- package/app/core/bookmeta.py +296 -0
- package/app/core/capability.py +130 -0
- package/app/core/catalog.py +319 -0
- package/app/core/compaction.py +186 -0
- package/app/core/diagnostics.py +115 -0
- package/app/core/env_scrub.py +84 -0
- package/app/core/error_codes.py +65 -0
- package/app/core/flows.py +328 -0
- package/app/core/gitmod.py +949 -0
- package/app/core/goal_service.py +159 -0
- package/app/core/health.py +294 -0
- package/app/core/history.py +32 -0
- package/app/core/jobs.py +424 -0
- package/app/core/manager.py +1415 -0
- package/app/core/market.py +299 -0
- package/app/core/market_remote.py +896 -0
- package/app/core/mocks.py +64 -0
- package/app/core/modelhub.py +2750 -0
- package/app/core/paths.py +60 -0
- package/app/core/pipeline.py +2161 -0
- package/app/core/planner.py +493 -0
- package/app/core/registry.py +105 -0
- package/app/core/remote.py +303 -0
- package/app/core/repeat_guard.py +124 -0
- package/app/core/router.py +120 -0
- package/app/core/runner.py +856 -0
- package/app/core/selfupdate.py +170 -0
- package/app/core/session_log.py +162 -0
- package/app/core/sessions.py +312 -0
- package/app/core/settings.py +85 -0
- package/app/core/settings_schema.py +250 -0
- package/app/core/skillpacks/fanqie-novel.md +80 -0
- package/app/core/skillpacks/market/character-bible.md +66 -0
- package/app/core/skillpacks/market/code-risk-checklist.md +58 -0
- package/app/core/skillpacks/market/git-workflow.md +57 -0
- package/app/core/skillpacks/market/release-notes.md +72 -0
- package/app/core/skillpacks/market/weekly-report.md +71 -0
- package/app/core/skillpacks/market/worldview-consistency.md +70 -0
- package/app/core/skillpacks/qimao-signing.md +105 -0
- package/app/core/skills.py +649 -0
- package/app/core/step_runner.py +61 -0
- package/app/core/store.py +1321 -0
- package/app/core/token_meter.py +130 -0
- package/app/core/usage.py +450 -0
- package/app/main.py +1448 -0
- package/app/ui/app.js +8021 -0
- package/app/ui/i18n.js +1709 -0
- package/app/ui/icons/brand-horizontal.png +0 -0
- package/app/ui/icons/brand-square.png +0 -0
- package/app/ui/icons/icon-192.png +0 -0
- package/app/ui/icons/icon-512.png +0 -0
- package/app/ui/icons/logo-horizontal.png +0 -0
- package/app/ui/icons/logo-mark.png +0 -0
- package/app/ui/index.html +864 -0
- package/app/ui/manifest.json +16 -0
- package/app/ui/qrcode.js +2297 -0
- package/app/ui/style.css +2733 -0
- package/bin/tutti.js +121 -0
- package/package.json +39 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""运行设置(data/settings.json):并发 worker 数 + 默认保存路径。"""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import threading
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from . import paths
|
|
10
|
+
|
|
11
|
+
_LOCK = threading.RLock()
|
|
12
|
+
_FILE = paths.DATA_DIR / "settings.json"
|
|
13
|
+
|
|
14
|
+
# default_workdir 为空表示未自定义,用 builtin_workdir() 回落
|
|
15
|
+
DEFAULTS = {"max_concurrent_jobs": 3, "default_workdir": ""}
|
|
16
|
+
MIN_WORKERS, MAX_WORKERS = 1, 6
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def builtin_workdir():
|
|
20
|
+
"""内置默认保存路径:<data 目录同级>/workspace。"""
|
|
21
|
+
return str(paths.DATA_DIR.parent / "workspace")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def default_workdir():
|
|
25
|
+
"""当前生效的默认保存路径(自定义优先,回落内置值)。"""
|
|
26
|
+
raw = str(load().get("default_workdir") or "").strip()
|
|
27
|
+
return raw if raw else builtin_workdir()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _norm_workdir(v):
|
|
31
|
+
"""展开 ~ 并转绝对路径;空值合法(回落内置)。返回 (标准化串, 错误)。"""
|
|
32
|
+
v = str(v or "").strip()
|
|
33
|
+
if not v:
|
|
34
|
+
return "", None
|
|
35
|
+
if v.startswith("~"):
|
|
36
|
+
v = str(Path(v).expanduser())
|
|
37
|
+
p = Path(v)
|
|
38
|
+
if not p.is_absolute():
|
|
39
|
+
return "", "默认保存路径必须是绝对路径"
|
|
40
|
+
return str(p), None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load():
|
|
44
|
+
with _LOCK:
|
|
45
|
+
try:
|
|
46
|
+
data = json.loads(_FILE.read_text(encoding="utf-8"))
|
|
47
|
+
except Exception:
|
|
48
|
+
data = {}
|
|
49
|
+
out = dict(DEFAULTS)
|
|
50
|
+
if isinstance(data, dict):
|
|
51
|
+
out.update({k: v for k, v in data.items() if k in DEFAULTS})
|
|
52
|
+
try:
|
|
53
|
+
out["max_concurrent_jobs"] = max(MIN_WORKERS, min(MAX_WORKERS, int(out["max_concurrent_jobs"])))
|
|
54
|
+
except Exception:
|
|
55
|
+
out["max_concurrent_jobs"] = DEFAULTS["max_concurrent_jobs"]
|
|
56
|
+
wd, _ = _norm_workdir(out.get("default_workdir"))
|
|
57
|
+
out["default_workdir"] = wd
|
|
58
|
+
out["default_workdir_effective"] = wd if wd else builtin_workdir()
|
|
59
|
+
return out
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def save(patch):
|
|
63
|
+
"""合并保存。返回 (view, 错误)。"""
|
|
64
|
+
if not isinstance(patch, dict):
|
|
65
|
+
return load(), "payload 必须是对象"
|
|
66
|
+
with _LOCK:
|
|
67
|
+
cur = load()
|
|
68
|
+
if "max_concurrent_jobs" in patch:
|
|
69
|
+
try:
|
|
70
|
+
cur["max_concurrent_jobs"] = int(patch["max_concurrent_jobs"])
|
|
71
|
+
except Exception:
|
|
72
|
+
return cur, "max_concurrent_jobs 必须是整数"
|
|
73
|
+
if not MIN_WORKERS <= cur["max_concurrent_jobs"] <= MAX_WORKERS:
|
|
74
|
+
return cur, "max_concurrent_jobs 取值 %d-%d" % (MIN_WORKERS, MAX_WORKERS)
|
|
75
|
+
if "default_workdir" in patch:
|
|
76
|
+
wd, err = _norm_workdir(patch.get("default_workdir"))
|
|
77
|
+
if err:
|
|
78
|
+
return cur, err
|
|
79
|
+
cur["default_workdir"] = wd
|
|
80
|
+
_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
tmp = _FILE.with_suffix(".tmp")
|
|
82
|
+
tmp.write_text(json.dumps(cur, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
83
|
+
tmp.replace(_FILE)
|
|
84
|
+
cur["default_workdir_effective"] = cur["default_workdir"] if cur["default_workdir"] else builtin_workdir()
|
|
85
|
+
return cur, None
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Schema 化设置:namespace 注册 + revision CAS + 脱敏 describe。
|
|
3
|
+
|
|
4
|
+
设计稿:docs/migration/05-model-seams.md §4C(按 Tutti 实际架构落地:
|
|
5
|
+
不替换现有 settings.py——它正被并行迭代且已有原子写+钳制;本模块提供
|
|
6
|
+
「schema 驱动 + revision CAS + secret 脱敏」能力,管理自己的
|
|
7
|
+
data/settings_v2.json,供新增配置项使用,存量 key-value 不迁移)。
|
|
8
|
+
|
|
9
|
+
参考 dsh packages/settings/settings/src/index.ts:419-637:
|
|
10
|
+
register(ns, schema, base) / mutate(ops) / describe({redactSecrets}) /
|
|
11
|
+
SettingsConflictError(revision 单调,乐观并发)。
|
|
12
|
+
|
|
13
|
+
冲突语义:mutate 带 expected_revision 时做 CAS;不带则最后写赢。
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
import threading
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
_LOCK = threading.RLock()
|
|
23
|
+
_FILE = None # init() 注入
|
|
24
|
+
_NAMESPACES = {} # ns -> {"fields": {path: FieldDef}, "validate": fn}
|
|
25
|
+
_VALUES = {} # ns -> {path: value}
|
|
26
|
+
_REVISIONS = {} # ns -> int
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SettingsConflictError(Exception):
|
|
30
|
+
"""CAS 失败:调用方持有的是陈旧 revision。"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FieldDef:
|
|
34
|
+
__slots__ = ("path", "ftype", "default", "description", "redact", "choices", "clamp")
|
|
35
|
+
|
|
36
|
+
def __init__(self, path, ftype, default, description="", redact=False,
|
|
37
|
+
choices=None, clamp=None):
|
|
38
|
+
self.path = path
|
|
39
|
+
self.ftype = ftype # "int" | "float" | "str" | "bool"
|
|
40
|
+
self.default = default
|
|
41
|
+
self.description = description
|
|
42
|
+
self.redact = redact # True = describe() 输出 __REDACTED__
|
|
43
|
+
self.choices = choices # 可选:合法值列表
|
|
44
|
+
self.clamp = clamp # 可选:(min, max),int/float 用
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def init(data_dir=None):
|
|
48
|
+
global _FILE, _NAMESPACES, _VALUES, _REVISIONS
|
|
49
|
+
with _LOCK:
|
|
50
|
+
base = Path(data_dir) if data_dir else Path(_default_dir())
|
|
51
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
_FILE = base / "settings_v2.json"
|
|
53
|
+
_NAMESPACES, _VALUES, _REVISIONS = {}, {}, {}
|
|
54
|
+
try:
|
|
55
|
+
data = json.loads(_FILE.read_text(encoding="utf-8"))
|
|
56
|
+
_VALUES = data.get("values") or {}
|
|
57
|
+
_REVISIONS = data.get("revisions") or {}
|
|
58
|
+
except (OSError, json.JSONDecodeError):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _default_dir():
|
|
63
|
+
from . import paths
|
|
64
|
+
return str(paths.DATA_DIR)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _persist():
|
|
68
|
+
if _FILE is None:
|
|
69
|
+
return
|
|
70
|
+
tmp = _FILE.with_suffix(".tmp")
|
|
71
|
+
tmp.write_text(json.dumps({"values": _VALUES, "revisions": _REVISIONS},
|
|
72
|
+
ensure_ascii=False, indent=2), encoding="utf-8")
|
|
73
|
+
tmp.replace(_FILE)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---- 路径读写(支持 "a.b" 点路径;不实现数组下标——Tutti 配置用不到) ----
|
|
77
|
+
|
|
78
|
+
def _get_path(d, path):
|
|
79
|
+
cur = d
|
|
80
|
+
for seg in path.split("."):
|
|
81
|
+
if not isinstance(cur, dict) or seg not in cur:
|
|
82
|
+
return None
|
|
83
|
+
cur = cur[seg]
|
|
84
|
+
return cur
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _set_path(d, path, value):
|
|
88
|
+
segs = path.split(".")
|
|
89
|
+
cur = d
|
|
90
|
+
for seg in segs[:-1]:
|
|
91
|
+
cur = cur.setdefault(seg, {})
|
|
92
|
+
if not isinstance(cur, dict):
|
|
93
|
+
raise ValueError("路径冲突:%s" % path)
|
|
94
|
+
cur[segs[-1]] = value
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---- 注册 ----
|
|
98
|
+
|
|
99
|
+
def register_namespace(ns, fields, validate=None):
|
|
100
|
+
"""注册 namespace 与字段 schema。已注册的 namespace 不可重复注册。"""
|
|
101
|
+
with _LOCK:
|
|
102
|
+
if ns in _NAMESPACES:
|
|
103
|
+
raise ValueError("namespace 已注册:%s" % ns)
|
|
104
|
+
fmap = {}
|
|
105
|
+
for f in fields:
|
|
106
|
+
fmap[f.path] = f
|
|
107
|
+
if _get_path(_VALUES.get(ns, {}), f.path) is None:
|
|
108
|
+
_VALUES.setdefault(ns, {})
|
|
109
|
+
_set_path(_VALUES[ns], f.path, f.default)
|
|
110
|
+
_NAMESPACES[ns] = {"fields": fmap, "validate": validate}
|
|
111
|
+
_REVISIONS.setdefault(ns, 1)
|
|
112
|
+
_persist()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def coerce(field, value):
|
|
116
|
+
"""按字段类型强转 + 钳制 + choices 校验。返回 (规范化值, 错误)。"""
|
|
117
|
+
try:
|
|
118
|
+
if field.ftype == "int":
|
|
119
|
+
v = int(value)
|
|
120
|
+
elif field.ftype == "float":
|
|
121
|
+
v = float(value)
|
|
122
|
+
elif field.ftype == "bool":
|
|
123
|
+
v = bool(value) if not isinstance(value, str) else (
|
|
124
|
+
value.strip().lower() in ("1", "true", "yes", "on"))
|
|
125
|
+
else:
|
|
126
|
+
v = str(value)
|
|
127
|
+
except (TypeError, ValueError):
|
|
128
|
+
return None, "%s 必须是 %s" % (field.path, field.ftype)
|
|
129
|
+
if field.clamp:
|
|
130
|
+
lo, hi = field.clamp
|
|
131
|
+
v = max(lo, min(hi, v))
|
|
132
|
+
if field.choices is not None and v not in field.choices:
|
|
133
|
+
return None, "%s 必须是 %s 之一" % (field.path, field.choices)
|
|
134
|
+
return v, None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ---- 读写 ----
|
|
138
|
+
|
|
139
|
+
def get(ns, path=None):
|
|
140
|
+
with _LOCK:
|
|
141
|
+
vals = _VALUES.get(ns)
|
|
142
|
+
if vals is None:
|
|
143
|
+
return None
|
|
144
|
+
return _get_path(vals, path) if path else dict(vals)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def revision(ns):
|
|
148
|
+
with _LOCK:
|
|
149
|
+
return _REVISIONS.get(ns, 0)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def mutate(ns, ops, expected_revision=None):
|
|
153
|
+
"""写一组操作。ops = [{"op":"set","path":..., "value":...}, ...]。
|
|
154
|
+
|
|
155
|
+
值按注册的 FieldDef 强转/钳制/校验;namespace 有 validate 钩子时整体验证。
|
|
156
|
+
expected_revision 非 None 时做 CAS。成功返回新 revision。
|
|
157
|
+
"""
|
|
158
|
+
with _LOCK:
|
|
159
|
+
ndef = _NAMESPACES.get(ns)
|
|
160
|
+
if not ndef:
|
|
161
|
+
raise ValueError("namespace 未注册:%s" % ns)
|
|
162
|
+
if expected_revision is not None and _REVISIONS.get(ns) != expected_revision:
|
|
163
|
+
raise SettingsConflictError(
|
|
164
|
+
"%s 期望 rev %s,实际 %s" % (ns, expected_revision, _REVISIONS.get(ns)))
|
|
165
|
+
# 先全部校验,后统一写入(部分失败不落盘)
|
|
166
|
+
staged = []
|
|
167
|
+
for op in ops:
|
|
168
|
+
if op.get("op") != "set":
|
|
169
|
+
raise ValueError("仅支持 set 操作:%r" % op.get("op"))
|
|
170
|
+
path = op.get("path")
|
|
171
|
+
fdef = ndef["fields"].get(path)
|
|
172
|
+
if not fdef:
|
|
173
|
+
raise ValueError("未注册字段:%s" % path)
|
|
174
|
+
v, err = coerce(fdef, op.get("value"))
|
|
175
|
+
if err:
|
|
176
|
+
raise ValueError(err)
|
|
177
|
+
staged.append((path, v))
|
|
178
|
+
ns_values = dict(_VALUES.get(ns, {}))
|
|
179
|
+
for path, v in staged:
|
|
180
|
+
_set_path(ns_values, path, v)
|
|
181
|
+
if ndef["validate"]:
|
|
182
|
+
err = ndef["validate"](ns_values)
|
|
183
|
+
if err:
|
|
184
|
+
raise ValueError("namespace 校验失败:%s" % err)
|
|
185
|
+
_VALUES[ns] = ns_values
|
|
186
|
+
_REVISIONS[ns] = _REVISIONS.get(ns, 1) + 1
|
|
187
|
+
_persist()
|
|
188
|
+
return _REVISIONS[ns]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def describe(ns, redact_secrets=True):
|
|
192
|
+
"""给 UI 的视图:默认把 redact 字段替换为 __REDACTED__(不泄露真值)。"""
|
|
193
|
+
with _LOCK:
|
|
194
|
+
vals = json.loads(json.dumps(_VALUES.get(ns, {}))) # deep copy
|
|
195
|
+
ndef = _NAMESPACES.get(ns) or {"fields": {}}
|
|
196
|
+
for f in ndef["fields"].values():
|
|
197
|
+
if f.redact and redact_secrets:
|
|
198
|
+
if _get_path(vals, f.path) not in (None, ""):
|
|
199
|
+
_set_path(vals, f.path, "__REDACTED__")
|
|
200
|
+
return {"ns": ns, "revision": _REVISIONS.get(ns, 0), "values": vals}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---- 默认 namespace(编排阈值的 schema 化存放处;pipeline 可渐进接入) ----
|
|
204
|
+
|
|
205
|
+
def register_default_namespaces():
|
|
206
|
+
"""注册 Tutti 默认 namespace。幂等(已注册则跳过)。"""
|
|
207
|
+
if "orchestrator" in _NAMESPACES and "budget" in _NAMESPACES:
|
|
208
|
+
return
|
|
209
|
+
if "orchestrator" not in _NAMESPACES:
|
|
210
|
+
register_namespace(
|
|
211
|
+
"orchestrator",
|
|
212
|
+
fields=[
|
|
213
|
+
FieldDef("compaction.enabled", "bool", False,
|
|
214
|
+
"上下文压缩总开关(灰度:与 TUTTI_COMPACTION 任一开启即生效)"),
|
|
215
|
+
FieldDef("compaction.pressure_threshold", "float", 0.8,
|
|
216
|
+
"触发压缩的压力比阈值",
|
|
217
|
+
clamp=(0.1, 0.99)),
|
|
218
|
+
FieldDef("compaction.retain_tail_tokens", "int", 8000,
|
|
219
|
+
"压缩时保留尾部预算(token 估算)",
|
|
220
|
+
clamp=(0, 100000)),
|
|
221
|
+
FieldDef("max_goal_rounds", "int", 5,
|
|
222
|
+
"Goal 续行上限",
|
|
223
|
+
clamp=(1, 20)),
|
|
224
|
+
FieldDef("outline_timeout_s", "int", 900,
|
|
225
|
+
"大纲/规划 CLI 兜底调用的超时秒数(env TUTTI_OUTLINE_TIMEOUT 优先)",
|
|
226
|
+
clamp=(60, 3600)),
|
|
227
|
+
],
|
|
228
|
+
validate=lambda v: None,
|
|
229
|
+
)
|
|
230
|
+
if "budget" not in _NAMESPACES:
|
|
231
|
+
register_namespace(
|
|
232
|
+
"budget",
|
|
233
|
+
fields=[
|
|
234
|
+
# §07 T2.1:0 = 不限;达到上限后 pipeline 停止后续真实调用(ENV_BLOCK)
|
|
235
|
+
FieldDef("max_tokens_per_run", "int", 0,
|
|
236
|
+
"单次运行 token 预算上限(0=不限;超额停止后续步骤,等自动续跑)",
|
|
237
|
+
clamp=(0, 100_000_000)),
|
|
238
|
+
],
|
|
239
|
+
validate=lambda v: None,
|
|
240
|
+
)
|
|
241
|
+
if "cascade" not in _NAMESPACES:
|
|
242
|
+
register_namespace(
|
|
243
|
+
"cascade",
|
|
244
|
+
fields=[
|
|
245
|
+
# §07 T3.1:easy 任务先走低档模型,质量闸门不过沿 chain 升级
|
|
246
|
+
FieldDef("enabled", "bool", False,
|
|
247
|
+
"FrugalGPT 式级联:easy 任务按 tier 升序重排模型链(需供应商声明 tier)"),
|
|
248
|
+
],
|
|
249
|
+
validate=lambda v: None,
|
|
250
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# 番茄小说写作与流量守则(内置经验包)
|
|
2
|
+
|
|
3
|
+
来源:番茄作家社区与作者群体的通行经验汇编 + 平台公开运营机制(2025)。
|
|
4
|
+
签约字数、保底金额、全勤门槛、审核时长等**政策数字随年份调整,一律以「番茄作家后台」当前规则为准**,
|
|
5
|
+
本包不固化具体数值。这是写作参考,不是官方规则,也不是爆款预测;不以格式检查推断过签/起量概率。
|
|
6
|
+
|
|
7
|
+
## 〇、与传统编辑平台的根本区别:番茄是算法平台
|
|
8
|
+
- 老牌免费站(编辑审稿制)= **编辑守门**(人审稿定签约);番茄 = **算法守门**(数据定生死)。
|
|
9
|
+
签约只是门票,签约之后靠完读率/追读率挣流量,才真正决定这本书的死活。
|
|
10
|
+
- 读者画像:下沉市场、碎片时间、耐心极低——**弃书零成本**(免费,右上角一点就走)。
|
|
11
|
+
开局漏斗比任何平台都陡:书名 → 简介/标签 → 第一章 → 前三章,每关都在大量筛人。
|
|
12
|
+
- 收入模型 = 免费阅读 + 广告分成(读者读得越久、追得越多,广告收益越高),
|
|
13
|
+
叠加签约保底/全勤(若合同有)。所以 **追读率 > 文笔 > 一切自我感动**。
|
|
14
|
+
|
|
15
|
+
## 一、新书生存线:流量池与数据指标
|
|
16
|
+
- 通行理解的机制:新书发布后先进**小流量测试**,按数据逐级放量(书架/推荐/搜索);
|
|
17
|
+
数据不达标就被限流直至"饿死"。**前 30 天左右的新书期是定生死窗口**。
|
|
18
|
+
- 后台核心指标(以作家后台实际展示为准):**完读率**(本章读完比例,决定能否进下一流量层)、
|
|
19
|
+
**追读率**(读了开头还在追最新章的比例,决定推荐是否持续)、留存、平均阅读时长。
|
|
20
|
+
- 数据异常按段落定位病因:第一章完读低 = 开头崩(改开篇);中段崩 = 节奏/爽点密度不足;
|
|
21
|
+
结尾追读崩 = 断章钩子或大坑久拖不兑现。**不要用"再等等"掩盖结构性问题**。
|
|
22
|
+
- 反恐慌纪律:头几天数据小、噪声大,不要 48 小时一改动;到样本足够(约两周)仍差,
|
|
23
|
+
**果断切书开新**——算法平台的沉没成本陷阱比编辑平台大得多。
|
|
24
|
+
|
|
25
|
+
## 二、开篇(黄金三章,番茄从严)
|
|
26
|
+
- 番茄测的是**整前三章**的完读与追读,不是单看第一章:第一章钩人、第二章兑现金手指/核心设定、
|
|
27
|
+
第三章第一个完整爽点闭环,缺一章都撑不起数据。
|
|
28
|
+
- 前 100 字:主角 + 具体冲突/异常直接进场;前 600 字:困境压到脸上。
|
|
29
|
+
- 开篇禁忌(每条都有对应数据崩法):世界观说明文(第一章完读崩)、慢热日常(第二章崩)、
|
|
30
|
+
多视角群像(读者找不到主角,追读崩)、倒叙解谜(下沉读者直接弃)。
|
|
31
|
+
|
|
32
|
+
## 三、节奏与爽点
|
|
33
|
+
- **每章至少一个爽点或情绪波动 + 章尾强钩子**;番茄章节奏建议 1800-2500 字/章(短平快,
|
|
34
|
+
卡阅读间隙),别写 4000 字大章考验耐心。
|
|
35
|
+
- 主线必须是"一句话能讲清的直线":目标明确、单线推进、读者永远知道主角下一步要干什么。
|
|
36
|
+
多线叙事、慢热铺垫在番茄大概率数据判死刑。
|
|
37
|
+
- 前 100 章要完成一次大爽点闭环(打脸/升级/身份跃迁的大兑现);过渡章连续两章以上无兑现,
|
|
38
|
+
追读率就会肉眼可见地掉。
|
|
39
|
+
- 跨平台通用的铁律仍然有效:主角主观能动性红线、打压为爆发服务、反派不能无脑。
|
|
40
|
+
|
|
41
|
+
## 四、题材、书名、标签与简介
|
|
42
|
+
- 番茄大盘热门题材带(男频):都市高武/都市脑洞、历史穿越、玄幻、悬疑脑洞(民俗/规则怪谈系在此,
|
|
43
|
+
守村人这类民俗悬疑素材适合往"悬疑脑洞"标签靠);女频:脑洞言情、年代、萌宝。题材热度轮动快,
|
|
44
|
+
开书前扫一次当季榜单。
|
|
45
|
+
- **分类/标签是流量匹配的第一步**:挂错分类 = 推给错的人 = 数据全差且查不出原因。
|
|
46
|
+
书名、简介、标签三者对题材的表达必须一致(算法按这些做人群匹配)。
|
|
47
|
+
- 书名策略:长尾、带情绪、带题材关键词;简介三要素:身份处境 + 核心冲突 + 悬念钩子。
|
|
48
|
+
|
|
49
|
+
## 五、更新纪律(算法平台命门)
|
|
50
|
+
- **日更稳定 > 单日爆更**:断更打断追读链,数据直接塌方;全勤/推荐权重普遍与稳定日更挂钩
|
|
51
|
+
(具体门槛看当前合同与后台规则)。
|
|
52
|
+
- 存稿 ≥10 万字再发书;新书期若存稿允许,日双更冲数据——推荐系统吃"活跃 + 追读"信号。
|
|
53
|
+
- 卡文宁可降纲不降质:拆未来 3 章的单元(目标→行动→阻碍→反转),别水字数糊弄——
|
|
54
|
+
水章的完读率会当场出卖你。
|
|
55
|
+
|
|
56
|
+
## 六、签约与合同
|
|
57
|
+
- 申请门槛(字数)、审核时长以作家后台当前规则为准;常见审核材料 = 开篇正文 + 大纲,
|
|
58
|
+
大纲别只写一句"后面上架爆火",要有明确的阶段目标与兑现节点。
|
|
59
|
+
- 类型以**分成为主**,优质作品可谈保底;签前重点看清 **影视/动漫/版权衍生授权范围**与独家条款、
|
|
60
|
+
违约与切书条款——免费平台合同中 IP 衍生让渡比例是历来争议集中点。
|
|
61
|
+
- 拒签自救:先归因(题材带匹配?开篇数据思维?大纲兑现链缺失?),改完再申;
|
|
62
|
+
同一作品反复无修改重投没有意义。
|
|
63
|
+
|
|
64
|
+
## 七、文笔自查(跨平台通用,2026-09-14 编辑驳回语实证拆解)
|
|
65
|
+
- **风格统一**:全篇定一个文风锚(短句+白描+口语但不过火),换场不换笔调;
|
|
66
|
+
文白/网络梗/书面长句不混用。
|
|
67
|
+
- **流畅**:逐段读出声——长定语、多重倒装、连续同主语开头(她…她…她…)、动作与反应拆碎,
|
|
68
|
+
卡顿处就地删改;一句一动作,段尾留半句承接。
|
|
69
|
+
- **细节刻画力度**:每个爽点/反转标记"特写镜头",五感 + 微动作 + 环境反馈把爆发那几秒写厚,
|
|
70
|
+
铺垫在前、放大在中、余波在后;过渡章才可一笔带过。**"每章一小爽"不配特写等于没写**。
|
|
71
|
+
|
|
72
|
+
## 八、开书/投稿前自检清单
|
|
73
|
+
- [ ] 书名/标签/简介一致表达题材,分类挂对
|
|
74
|
+
- [ ] 第 1 章前 100 字冲突进场;前三章 = 钩子 + 设定兑现 + 完整爽点闭环
|
|
75
|
+
- [ ] 每章 1800-2500 字,含至少一个爽点 + 章尾钩子
|
|
76
|
+
- [ ] 主线一句话讲得清,单线推进,无慢热铺垫章节
|
|
77
|
+
- [ ] 前 100 章有一次大兑现闭环的规划
|
|
78
|
+
- [ ] 存稿 ≥10 万字;日更承诺可执行(含请假预案)
|
|
79
|
+
- [ ] 文笔三词(风格统一/流畅/细节力度)逐章自查过
|
|
80
|
+
- [ ] 合同衍生授权与独家条款已看清
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 角色小传与人物弧光模板
|
|
3
|
+
note: 角色档案模板(欲望/恐惧/语言指纹)+ 四拍弧光规划 + 连载防崩人设纪律与自检
|
|
4
|
+
scopes:
|
|
5
|
+
- novel
|
|
6
|
+
- serial_novel
|
|
7
|
+
source: market
|
|
8
|
+
market_id: character-bible
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# 角色小传与人物弧光模板(市场包)
|
|
12
|
+
|
|
13
|
+
适用:开书前建角色档案、连载中防人设漂移。与平台流量包互补:流量包管
|
|
14
|
+
「读者为什么追」,本包管「人物为什么可信」。人设崩塌带来的流失不可逆——
|
|
15
|
+
评论区第一次出现「OOC」「崩人设」时,掉的数据基本回不来。
|
|
16
|
+
|
|
17
|
+
## 一、小传模板(有名有姓的角色都建档,配角用精简版)
|
|
18
|
+
- **基本档案**:姓名 / 年龄 / 身份职业 / 外貌。外貌只挑 ≤3 个**有辨识度且可反复
|
|
19
|
+
使用**的特征(疤、跛、口音、总戴手套),不要「很帅」「很美」这类无法复用的词。
|
|
20
|
+
- **表层目标**:TA 自己以为想要的东西(金银、复仇、上岸)。
|
|
21
|
+
- **深层需求**:真正缺的东西(认同、安全感、被看见)——弧光的终点就是发现并得到它。
|
|
22
|
+
- **恐惧与软肋**:最怕失去什么;软肋必须**情节可攻击**(有家人的反派才有戏可写)。
|
|
23
|
+
- **创伤与信念**:一句成型的信条(如「强才能活」),角色一切非常规行为的解释源头。
|
|
24
|
+
- **语言指纹**:口头禅 / 句长习惯(文绉绉还是粗短句)/ 禁用词(读书人不说粗话)/
|
|
25
|
+
称呼习惯(怎么叫主角)。这是对白区分度最硬的抓手。
|
|
26
|
+
- **关系网**:与主要角色的关系各一句话 + 张力在哪(靠什么维系、因什么会破裂)。
|
|
27
|
+
|
|
28
|
+
## 二、弧光规划(主角必须有,重要配角推荐)
|
|
29
|
+
- 四拍结构:**起点状态**(用旧信念应对世界)→ **触发事件**(旧信念失灵的一击)→
|
|
30
|
+
**中间摇摆**(至少两次反复:尝试改变,再退回舒适区,不许直线转变)→
|
|
31
|
+
**终点状态**(新信念,且由高潮事件证明成立)。
|
|
32
|
+
- 弧光要「可拍摄」:终点状态必须体现为**行为变化**(以前遇辱必怒,现在能先忍住
|
|
33
|
+
查真相),不能靠内心独白宣布「他长大了」。
|
|
34
|
+
- 反派同样要逻辑自洽:反派在自己的故事里是合理的(有目标、有原则、有能力),
|
|
35
|
+
**反派不无脑**;反派降智 = 主角胜利贬值 = 全书爽点贬值。
|
|
36
|
+
|
|
37
|
+
## 三、防崩纪律(连载中执行)
|
|
38
|
+
- 写对白前回看该角色的「语言指纹」;**遮名测试**:不同角色的对白遮住名字能认出
|
|
39
|
+
是谁说的,认不出 = 语言指纹失败,回去改。
|
|
40
|
+
- 配角也要「想要什么」:出场就带着自己的目的行动,哪怕只写一句。
|
|
41
|
+
只在剧情需要时才动的工具人,是群像发糊的根源。
|
|
42
|
+
- **每 10 章回看小传**:外貌、伤势、随身物、人际关系与正文逐一核对——伤过的手
|
|
43
|
+
不能无解释痊愈,卖掉的东西不能再掏出来。
|
|
44
|
+
- 能力升级必须有过程与代价;「突然会了」读者瞬间就能感知。
|
|
45
|
+
- 人物相关设定要改,必须回填小传并排查前文旧描述(与一致性台账守则联动)。
|
|
46
|
+
|
|
47
|
+
## 四、配角精简模板(三行建档)
|
|
48
|
+
- 一句话身份 + 此刻想要什么(出场动机)。
|
|
49
|
+
- 一个可复用的辨识特征(外貌 / 口头禅 / 习惯动作)。
|
|
50
|
+
- 与主要角色的关系张力(靠什么维系、因什么破裂)。
|
|
51
|
+
三行都填不出来 = 这个角色还不该出场;信息攒够再让他登场。
|
|
52
|
+
|
|
53
|
+
## 五、小传模板示例(可直接复制)
|
|
54
|
+
> **姓名**:xxx **年龄 / 身份**:xxx
|
|
55
|
+
> **表层目标**:…… **深层需求**:……
|
|
56
|
+
> **恐惧软肋**:…… **信条**:……
|
|
57
|
+
> **语言指纹**:口头禅……;句式……;禁语……
|
|
58
|
+
> **关系网**:与主角——……;与反派——……
|
|
59
|
+
> **弧光四拍**:起点…… → 触发事件…… → 摇摆…… → 终点……
|
|
60
|
+
|
|
61
|
+
## 六、写作前自检
|
|
62
|
+
- [ ] 本章每个出场角色都知道自己「想要什么」,行为在为目标服务
|
|
63
|
+
- [ ] 对白符合各自语言指纹(遮名测试通过)
|
|
64
|
+
- [ ] 主角行为符合当前弧光阶段:没提前顿悟、没无铺垫地反复横跳
|
|
65
|
+
- [ ] 反派行为在其自身逻辑内成立,没有为剧情方便而降智
|
|
66
|
+
- [ ] 外貌 / 伤势 / 随身物与前文一致,本章新信息已记入小传
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 代码风险自查清单
|
|
3
|
+
note: 提测/评审前的风险过单:边界与异常 / 资源与并发 / 安全 / 兼容与回退
|
|
4
|
+
scopes:
|
|
5
|
+
- "*"
|
|
6
|
+
source: market
|
|
7
|
+
market_id: code-risk-checklist
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# 代码风险自查清单(市场包)
|
|
11
|
+
|
|
12
|
+
适用:提交、提测、评审前对**自己写的代码**做一遍系统性过单。条目按事故率排序,
|
|
13
|
+
每一条都对应一类反复发生的线上事故。用法:逐项问「我的改动碰到这条了吗」,
|
|
14
|
+
碰到了就必须有明确答案,**不许「应该没事」**。
|
|
15
|
+
|
|
16
|
+
## 一、边界与异常(最高频事故源)
|
|
17
|
+
- 空值:入参为 None / 空串 / 空列表 / 空对象时,每一层调用会怎样?
|
|
18
|
+
dict 取键、列表取 [0]、`a.b.c` 属性链是最常见的崩点。
|
|
19
|
+
- 数值边界:0、负数、极大值、小数精度(金额与计数一律避免 float 裸算的累计误差)。
|
|
20
|
+
- 超长与超量:超长字符串、上千条的批量输入、没做分页的全量查询。
|
|
21
|
+
- 异常处理红线:**禁止裸 `except: pass` 吞异常**——要么处理(记日志 + 降级/重试),
|
|
22
|
+
要么让它炸出来。吞掉的异常等于把今天的报错换成下周的灵异故障。
|
|
23
|
+
- 失败路径:主流程成功不算完,逐问「每一步失败后,状态是否仍然一致」——
|
|
24
|
+
写了一半的文件、扣了款没发货的订单、删了源没删索引。
|
|
25
|
+
|
|
26
|
+
## 二、资源与并发
|
|
27
|
+
- 打开的文件 / 连接 / 锁,在**所有**退出路径上都关闭了吗(含异常路径)?
|
|
28
|
+
用 with / try-finally / defer 这类语言级保证,不靠「记得 close」。
|
|
29
|
+
- 循环内不做 IO / 网络 / 建连接(N+1 模式);能批量就批量。
|
|
30
|
+
- 并发共享状态:全局变量、模块级缓存、多线程写同一文件——有锁吗?锁的粒度
|
|
31
|
+
会不会死锁?**单测通过不代表并发安全**,并发问题要靠设计保证而不是靠运气。
|
|
32
|
+
- 幂等性:重试、重复请求、消息重复消费时,同一操作执行两次结果必须不变
|
|
33
|
+
(尤其支付、发送、计数类操作)。
|
|
34
|
+
|
|
35
|
+
## 三、安全(每次改动都扫一眼)
|
|
36
|
+
- 拼 SQL / 拼命令 / 拼 HTML = 注入。参数化或转义,没有例外。
|
|
37
|
+
- 路径拼接必须校验:用户可控输入参与拼路径时防目录穿越(先 resolve 再判前缀,
|
|
38
|
+
或白名单校验文件名)。
|
|
39
|
+
- 密钥与令牌不硬编码、不打日志;日志里的手机号 / 身份证 / 邮箱脱敏。
|
|
40
|
+
- 对外接口:入参校验(类型 / 长度 / 范围);错误信息不回显内部堆栈与路径。
|
|
41
|
+
- 新增依赖必须来源可信、版本锁定;升级依赖后过一眼 changelog 的破坏性变更。
|
|
42
|
+
|
|
43
|
+
## 四、兼容与可回退
|
|
44
|
+
- 跨平台:路径分隔符(用语言自带的路由 API,不手拼 `/` 或 `\`)、文件编码
|
|
45
|
+
(Windows 默认 GBK,读写文件显式 UTF-8)、换行符、文件名大小写敏感差异。
|
|
46
|
+
- 数据兼容:改字段名 / 改格式时,旧数据还读得动吗?迁移脚本在哪、可重跑吗?
|
|
47
|
+
- 接口兼容:改出参、删字段之前,确认所有调用方(前端、脚本、第三方)已同步修改。
|
|
48
|
+
- 可回退:这次改动出问题,怎么回到上一版本?配置开关、灰度、revert 方案,
|
|
49
|
+
动手前至少想清楚一个并写进提交说明。
|
|
50
|
+
|
|
51
|
+
## 五、提测前过单(逐项打勾,全绿才提交评审)
|
|
52
|
+
- [ ] 空值 / 边界入参各试过一次:不崩、行为合理
|
|
53
|
+
- [ ] 无裸吞异常;所有异常路径上资源正确释放
|
|
54
|
+
- [ ] 无循环内 IO;并发路径有并发保护或明确论证过只在单线程运行
|
|
55
|
+
- [ ] 无拼接 SQL / 命令;无硬编码密钥;日志无敏感明文
|
|
56
|
+
- [ ] 路径 / 编码 / 换行已跨平台处理(Windows 环境必查)
|
|
57
|
+
- [ ] 已想好并写下回退方式(revert / 开关 / 迁移脚本)
|
|
58
|
+
- [ ] 新增或修改的逻辑有对应测试,改动的测试全部通过
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Git 提交与分支守则
|
|
3
|
+
note: 提交信息规范 / 分支模型 / 危险操作红线 / 冲突与回滚 / 提交前自检清单
|
|
4
|
+
scopes:
|
|
5
|
+
- "*"
|
|
6
|
+
source: market
|
|
7
|
+
market_id: git-workflow
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Git 提交与分支守则(市场包)
|
|
11
|
+
|
|
12
|
+
适用:任何带版本控制的工程或写作任务。目标只有一个——**让半年后的自己(和协作者)
|
|
13
|
+
能从提交历史里读懂「为什么改」,并且任何时候都敢放心回滚**。本包约束的是纪律,
|
|
14
|
+
不是 git 教程;每条红线背后都是一类真实发生过的事故。
|
|
15
|
+
|
|
16
|
+
## 一、提交信息(Commit Message)
|
|
17
|
+
- 格式:`类型(范围): 一句话摘要`。类型闭集:feat / fix / docs / refactor / test / chore,
|
|
18
|
+
不自创类型(update、modify、change 这类词无法被工具统计与检索)。
|
|
19
|
+
- 摘要 ≤50 字,祈使句,结尾不加句号;写「做了什么」,不写「改了哪些文件」。
|
|
20
|
+
反例:`修改了一些文件`;正例:`fix(api): 空请求体导致 500 的问题`。
|
|
21
|
+
- 摘要说不清的改动,空一行后写 body:解释**为什么改**(背景、权衡、副作用),
|
|
22
|
+
不要复述 diff 本身能看出来的「改了什么」。
|
|
23
|
+
- 一次提交只做一件事:修 bug 的提交里不顺手重构;发现顺手该改的,另起提交。
|
|
24
|
+
混合提交无法单独回滚,是最常见的 git 事故根源。
|
|
25
|
+
|
|
26
|
+
## 二、分支模型
|
|
27
|
+
- `main`(或 master)永远可发布:只接受经过验证的合并,禁止把未验证的功能代码
|
|
28
|
+
直接 push 上去。
|
|
29
|
+
- 分支命名能看出目的:功能 `feat/<短横线描述>`,修复 `fix/<短横线描述>`;
|
|
30
|
+
一个分支只对应一个目的,合完即删——活过两周的分支几乎必然产生严重冲突。
|
|
31
|
+
- 同步纪律:动手前先拉最新主干;分支生命周期超过 2 天,每天至少同步一次主干,
|
|
32
|
+
冲突在自己分支上解决,不把冲突攒到合并时刻。
|
|
33
|
+
|
|
34
|
+
## 三、危险操作红线
|
|
35
|
+
- 禁止对**已推送的公共分支**做 `reset` / `amend` / force push——重写历史会让所有
|
|
36
|
+
协作者的本地仓库错乱。公共分支上的错误用 `git revert` 生成反向提交。
|
|
37
|
+
- `git push --force` 只允许推自己的功能分支;确需覆盖时用 `--force-with-lease`
|
|
38
|
+
(远端有别人新提交时会拒绝,防误伤)。
|
|
39
|
+
- 提交前不许 `git add -A` 了事:先 `git status` + `git diff --staged` 逐块确认,
|
|
40
|
+
防止把临时脚本、日志、密钥、二进制大文件带进历史(进了历史几乎删不干净)。
|
|
41
|
+
- 敏感信息(密钥、口令、内网地址)一旦提交,**立即视为已泄露**:先作废并轮换凭据,
|
|
42
|
+
再清理历史,顺序不能反——清理历史赶不上爬虫抓取的速度。
|
|
43
|
+
|
|
44
|
+
## 四、冲突与回滚
|
|
45
|
+
- 冲突解决以**业务正确**为准,不以「保留谁写的」为准;解决完必须编译/跑测通过
|
|
46
|
+
再提交,冲突盲选是隐性 bug 的最大来源。
|
|
47
|
+
- 回滚决策树:未推送 → `reset`(本地随意);已推送未发布 → `revert` 反向提交;
|
|
48
|
+
已发布出问题 → 先 revert 恢复线上,再慢慢定位,**不在主干上现场调试**。
|
|
49
|
+
- 大改动合并前先试合:`git merge --no-commit --no-ff` 预览,或在临时分支上合一遍。
|
|
50
|
+
|
|
51
|
+
## 五、提交前自检清单
|
|
52
|
+
- [ ] `git status` 干净:没有预期外的未跟踪/被修改文件
|
|
53
|
+
- [ ] `git diff --staged` 逐块看过:无调试代码、无密钥、无大文件
|
|
54
|
+
- [ ] 提交信息符合 `类型(范围): 摘要`,摘要 ≤50 字且脱离上下文能看懂
|
|
55
|
+
- [ ] 一次提交一个意图;无关改动已拆分
|
|
56
|
+
- [ ] 功能分支基于最新主干;公共分支未被重写历史
|
|
57
|
+
- [ ] 提交后本地测试/CI 通过;红了立即 fix 或 revert,不留红主干
|