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,299 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""插件市场(Market):经验库(skills)之上的「发现 + 一键安装」层。
|
|
3
|
+
|
|
4
|
+
分工:skills 负责包的解析 / scope 匹配 / 注入 / 启停(运行机制),市场只负责
|
|
5
|
+
「发现与安装」——把人工维护的包目录(BUILTIN_PACKS)展示出来,用户点一下就把
|
|
6
|
+
包内容写进 data/skillpacks/,之后的一切(frontmatter 解析、按流程类型注入、启停)
|
|
7
|
+
全部复用 skills 的既有机制,市场不自建注入通道。
|
|
8
|
+
|
|
9
|
+
数据约定:
|
|
10
|
+
- 目录内容源:app/core/skillpacks/market/<pack_id>.md(与内置包同一棵目录树,
|
|
11
|
+
frontmatter 里带 source: market / market_id: <id> 作为安装标记);
|
|
12
|
+
- 安装目标:data/skillpacks/market-<pack_id>.md(skills 的用户包目录;market-
|
|
13
|
+
前缀 + frontmatter 标记双保险,绝不覆盖用户自建文件,重名自动避让换名);
|
|
14
|
+
- 安装状态:data/market.json(原子写),记录装过哪些包、装到了哪个文件;
|
|
15
|
+
记录丢失时按文件里的 market 标记扫描自愈(installed_ids)。
|
|
16
|
+
卸载只认「market.json 有记录 + 文件带 market 标记」的包;skills 内置包
|
|
17
|
+
(app/core/skillpacks/ 下)与无标记的用户自建包一律拒绝删除。
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import re
|
|
23
|
+
import shutil
|
|
24
|
+
import threading
|
|
25
|
+
import time
|
|
26
|
+
|
|
27
|
+
from . import paths, skills
|
|
28
|
+
|
|
29
|
+
_LOCK = threading.RLock()
|
|
30
|
+
|
|
31
|
+
# 目录内容的源目录:与 skills 内置包同一棵目录树(app/core/skillpacks/),子目录分家
|
|
32
|
+
_SRC_DIR = paths.APP_DIR / "core" / "skillpacks" / "market"
|
|
33
|
+
|
|
34
|
+
# 文件 frontmatter 里的安装标记(本模块自己写的格式,正则足够)
|
|
35
|
+
_MARKER_RE = re.compile(r"(?m)^market_id:\s*([A-Za-z0-9_.-]+)\s*$")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _registry_file():
|
|
39
|
+
"""安装状态文件:每次现取 paths.DATA_DIR(测试重定向后自动跟随)。"""
|
|
40
|
+
return paths.DATA_DIR / "market.json"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _user_pack_dir():
|
|
44
|
+
"""skills 的用户包目录(安装目标),随 paths.DATA_DIR 现取。"""
|
|
45
|
+
return paths.DATA_DIR / "skillpacks"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _now():
|
|
49
|
+
return time.strftime("%Y-%m-%d %H:%M:%S")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------- 包目录(catalog)
|
|
53
|
+
|
|
54
|
+
# 可安装的市场包目录:内容源文件在 skillpacks/market/<id>.md,import 时读入 files
|
|
55
|
+
# ({安装相对路径: 正文})。category 必须取 skills.LESSON_CATEGORIES 闭集枚举值。
|
|
56
|
+
BUILTIN_PACKS = [
|
|
57
|
+
{"id": "git-workflow", "name": "Git 提交与分支守则",
|
|
58
|
+
"desc": "提交信息格式与粒度、分支模型、force push 与回滚等危险操作红线,附提交前自检清单。",
|
|
59
|
+
"category": "流程规范", "scopes": ["*"], "file": "git-workflow.md"},
|
|
60
|
+
{"id": "code-risk-checklist", "name": "代码风险自查清单",
|
|
61
|
+
"desc": "提测/评审前按事故率过单:边界与异常、资源与并发、注入与泄密、跨平台与回退。",
|
|
62
|
+
"category": "流程规范", "scopes": ["*"], "file": "code-risk-checklist.md"},
|
|
63
|
+
{"id": "release-notes", "name": "版本发布说明撰写模板",
|
|
64
|
+
"desc": "面向用户的更新公告写法:固定六段结构、破坏性变更三要素、可复制模板与反例对照。",
|
|
65
|
+
"category": "文笔风格", "scopes": ["*"], "file": "release-notes.md"},
|
|
66
|
+
{"id": "weekly-report", "name": "周报/晨报生成器守则",
|
|
67
|
+
"desc": "先取材再总结:从任务与运行记录提炼晨报三段、周报四段,量化纪律与可复制模板。",
|
|
68
|
+
"category": "流程规范", "scopes": ["*"], "file": "weekly-report.md"},
|
|
69
|
+
{"id": "character-bible", "name": "角色小传与人物弧光模板",
|
|
70
|
+
"desc": "角色档案模板(欲望/恐惧/语言指纹)、四拍弧光规划与连载防 OOC 纪律。",
|
|
71
|
+
"category": "人物塑造", "scopes": ["novel", "serial_novel"], "file": "character-bible.md"},
|
|
72
|
+
{"id": "worldview-consistency", "name": "世界观设定一致性台账守则",
|
|
73
|
+
"desc": "设定台账五件套、设定变更三步流程、高频吃书场景排查与章前查章后记闭环。",
|
|
74
|
+
"category": "一致性", "scopes": ["novel", "serial_novel"], "file": "worldview-consistency.md"},
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
# skills 里已内置的经验包 → 市场目录的展示信息(人工标注分类与一句话说明;
|
|
78
|
+
# skills 未来新增内置包时落「未分类」兜底,目录仍然可见、不可安装)
|
|
79
|
+
_BUILTIN_META = {
|
|
80
|
+
"fanqie-novel": {"category": "节奏爽点",
|
|
81
|
+
"desc": "算法流量池与完读追读、黄金三章整体验、题材标签匹配、更新纪律与合同要点。"},
|
|
82
|
+
"qimao-signing": {"category": "流程规范",
|
|
83
|
+
"desc": "七猫签约导向的写作规范:黄金一章、爽点纪律、期待感三源、人物红线与自检清单。"},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _read_source(fname):
|
|
88
|
+
"""读目录内容源文件(限制在本目录内,防路径逃逸);缺失返回空串。"""
|
|
89
|
+
try:
|
|
90
|
+
p = (_SRC_DIR / fname).resolve()
|
|
91
|
+
if _SRC_DIR.resolve() not in p.parents or not p.is_file():
|
|
92
|
+
return ""
|
|
93
|
+
return p.read_text(encoding="utf-8", errors="replace")
|
|
94
|
+
except Exception:
|
|
95
|
+
return ""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
for _p in BUILTIN_PACKS:
|
|
99
|
+
# files: {安装相对路径: 内容}——单文件包即一个顶层 .md(装后成为 skills 用户包);
|
|
100
|
+
# 未来带子目录相对路径的条目会作为随包资料落到 market-assets/(见 _dest_path)
|
|
101
|
+
_p["files"] = {"market-%s.md" % _p["id"]: _read_source(_p["file"])}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _dest_path(pack_id, rel):
|
|
105
|
+
"""files 的相对路径 → 安装目标:无子目录的落 data/skillpacks/ 顶层(成为可注入
|
|
106
|
+
用户包);带子目录的附加件落 data/skillpacks/market-assets/<id>/(skills 只扫
|
|
107
|
+
顶层 *.md,附加件作为随包资料不参与注入)。"""
|
|
108
|
+
rel = str(rel).replace("\\", "/")
|
|
109
|
+
if "/" not in rel:
|
|
110
|
+
return _user_pack_dir() / rel
|
|
111
|
+
return _user_pack_dir() / "market-assets" / pack_id / rel
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _is_market_file(path, pack_id):
|
|
115
|
+
"""文件是否带本包的 market 安装标记(frontmatter 的 source / market_id 两行)。"""
|
|
116
|
+
try:
|
|
117
|
+
head = path.read_text(encoding="utf-8", errors="replace")[:800]
|
|
118
|
+
except OSError:
|
|
119
|
+
return False
|
|
120
|
+
return "source: market" in head and ("market_id: %s" % pack_id) in head
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------- 安装状态(持久化)
|
|
124
|
+
|
|
125
|
+
def _load_registry():
|
|
126
|
+
try:
|
|
127
|
+
data = json.loads(_registry_file().read_text(encoding="utf-8"))
|
|
128
|
+
return data if isinstance(data, dict) else {}
|
|
129
|
+
except Exception:
|
|
130
|
+
return {}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _save_registry(data):
|
|
134
|
+
f = _registry_file()
|
|
135
|
+
f.parent.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
tmp = f.with_suffix(".tmp")
|
|
137
|
+
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
138
|
+
tmp.replace(f) # 原子替换(与 skills._save 同一手法)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _scan_marked_files():
|
|
142
|
+
"""扫 data/skillpacks/*.md 里的 market 安装标记:id → 文件名列表。
|
|
143
|
+
market.json 丢失(手工清理/换机)时据此自愈识别已装包。"""
|
|
144
|
+
found = {}
|
|
145
|
+
try:
|
|
146
|
+
for p in sorted(_user_pack_dir().glob("*.md")):
|
|
147
|
+
try:
|
|
148
|
+
head = p.read_text(encoding="utf-8", errors="replace")[:800]
|
|
149
|
+
except OSError:
|
|
150
|
+
continue
|
|
151
|
+
if "source: market" not in head:
|
|
152
|
+
continue
|
|
153
|
+
m = _MARKER_RE.search(head)
|
|
154
|
+
if m:
|
|
155
|
+
found.setdefault(m.group(1), []).append(p.name)
|
|
156
|
+
except OSError:
|
|
157
|
+
pass
|
|
158
|
+
return found
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def installed_ids():
|
|
162
|
+
"""市场已安装的包 id 集合 = market.json 记录 ∪ 文件标记扫描(自愈)。"""
|
|
163
|
+
with _LOCK:
|
|
164
|
+
ids = set((_load_registry().get("installed") or {}).keys())
|
|
165
|
+
ids.update(_scan_marked_files().keys())
|
|
166
|
+
return ids
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ---------------------------------------------------------------- 对外接口
|
|
170
|
+
|
|
171
|
+
def view():
|
|
172
|
+
"""市场目录(给 UI/API):{catalog: [...], categories: [...]}。
|
|
173
|
+
每项带 installed: bool;skills 内置包 installed=True 且 installable=False。"""
|
|
174
|
+
installed = installed_ids()
|
|
175
|
+
items = []
|
|
176
|
+
for p in skills.BUILTIN_PACKS: # 已内置:已存在,不可安装
|
|
177
|
+
meta = _BUILTIN_META.get(p["id"]) or {}
|
|
178
|
+
items.append({"id": p["id"], "name": p["name"],
|
|
179
|
+
"desc": meta.get("desc") or p.get("note") or "",
|
|
180
|
+
"category": meta.get("category") or skills.LESSON_UNCATEGORIZED,
|
|
181
|
+
"scopes": list(p.get("scopes") or ["*"]),
|
|
182
|
+
"builtin": True, "installed": True, "installable": False,
|
|
183
|
+
"chars": len(skills.pack_text(p))})
|
|
184
|
+
for p in BUILTIN_PACKS: # 可安装市场包
|
|
185
|
+
files = p.get("files") or {}
|
|
186
|
+
items.append({"id": p["id"], "name": p["name"], "desc": p["desc"],
|
|
187
|
+
"category": p["category"], "scopes": list(p["scopes"]),
|
|
188
|
+
"builtin": False, "installed": p["id"] in installed,
|
|
189
|
+
"installable": True, "files": sorted(files),
|
|
190
|
+
"chars": sum(len(v) for v in files.values())})
|
|
191
|
+
cats = list(skills.LESSON_CATEGORIES) # 闭集全枚举先给全(UI 下拉不缺项)
|
|
192
|
+
for it in items:
|
|
193
|
+
if it["category"] not in cats:
|
|
194
|
+
cats.append(it["category"])
|
|
195
|
+
return {"catalog": items, "categories": cats}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def install(pack_id):
|
|
199
|
+
"""安装市场包:把 files 写进 skills 的用户包目录(data/skillpacks/),
|
|
200
|
+
之后由 skills 正常解析、按 scopes 注入。返回 (结果 dict, 错误)。
|
|
201
|
+
|
|
202
|
+
已装过(记录在且文件仍是本包的)→ 幂等返回 already=True,不覆盖用户可能的
|
|
203
|
+
定制;记录在但文件丢了/被替换 → 换名重写修复,绝不覆盖无标记的用户文件;
|
|
204
|
+
未知 id 与 skills 内置包 → 报错。
|
|
205
|
+
"""
|
|
206
|
+
if any(p["id"] == pack_id for p in skills.BUILTIN_PACKS):
|
|
207
|
+
return None, "「%s」是内置经验包(已存在),无需安装" % pack_id
|
|
208
|
+
pack = next((p for p in BUILTIN_PACKS if p["id"] == pack_id), None)
|
|
209
|
+
if not pack:
|
|
210
|
+
return None, "市场目录中无此包: %s" % pack_id
|
|
211
|
+
files = pack.get("files") or {}
|
|
212
|
+
if not any(str(v).strip() for v in files.values()):
|
|
213
|
+
return None, "包内容缺失(skillpacks/market/%s 不存在或为空)" % pack.get("file")
|
|
214
|
+
return install_files(pack_id, pack["name"], files)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def install_files(pack_id, name, files, extra=None):
|
|
218
|
+
"""通用安装入口:任意 {安装相对路径: 文本内容} 写进用户包目录并记账。
|
|
219
|
+
内置市场包(install)与外部目录插件(market_remote)共用这一条落地通道,
|
|
220
|
+
避让 / 安装标记 / market.json 记账 / 幂等纪律完全一致;extra 追加进记账记录
|
|
221
|
+
(如外部插件的来源与版本)。"""
|
|
222
|
+
if not files or not any(str(v).strip() for v in files.values()):
|
|
223
|
+
return None, "包内容为空: %s" % pack_id
|
|
224
|
+
with _LOCK:
|
|
225
|
+
udir = _user_pack_dir()
|
|
226
|
+
reg = _load_registry()
|
|
227
|
+
installed = reg.setdefault("installed", {})
|
|
228
|
+
rec = installed.get(pack_id)
|
|
229
|
+
primary = "market-%s.md" % pack_id
|
|
230
|
+
target = (rec or {}).get("file") or ""
|
|
231
|
+
already = bool(target and _is_market_file(udir / target, pack_id))
|
|
232
|
+
if not already:
|
|
233
|
+
# 无记录 / 记录在但文件丢了或被替换:主文件选一个不踩用户文件的目标名
|
|
234
|
+
target = primary
|
|
235
|
+
n = 1
|
|
236
|
+
while (udir / target).exists() and not _is_market_file(udir / target, pack_id):
|
|
237
|
+
n += 1
|
|
238
|
+
target = "market-%s-%d.md" % (pack_id, n)
|
|
239
|
+
try:
|
|
240
|
+
udir.mkdir(parents=True, exist_ok=True)
|
|
241
|
+
written = []
|
|
242
|
+
for rel in sorted(files):
|
|
243
|
+
# 主文件落避让后的名字;带子目录的附加件按原相对路径落 assets
|
|
244
|
+
name_i = target if rel == primary else rel
|
|
245
|
+
dest = _dest_path(pack_id, name_i)
|
|
246
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
247
|
+
dest.write_text(files[rel], encoding="utf-8")
|
|
248
|
+
written.append(name_i)
|
|
249
|
+
except OSError as e:
|
|
250
|
+
return None, "写入用户技能库失败: %s" % e
|
|
251
|
+
record = {"file": target, "files": written, "installed_at": _now()}
|
|
252
|
+
if extra:
|
|
253
|
+
record.update(extra)
|
|
254
|
+
installed[pack_id] = record
|
|
255
|
+
_save_registry(reg)
|
|
256
|
+
return {"ok": True, "id": pack_id, "name": name, "file": target,
|
|
257
|
+
"already": already}, None
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def remove(pack_id):
|
|
261
|
+
"""卸载市场包:只删 market.json 有记录(或文件标记可自愈)且文件带本包
|
|
262
|
+
market 标记的包。skills 内置包与无标记的用户自建包一律拒绝。
|
|
263
|
+
返回 None=成功,字符串=错误(沿用 skills.pack_op 的错误风格)。"""
|
|
264
|
+
if any(p["id"] == pack_id for p in skills.BUILTIN_PACKS):
|
|
265
|
+
return "内置经验包不可从市场卸载: %s" % pack_id
|
|
266
|
+
with _LOCK:
|
|
267
|
+
reg = _load_registry()
|
|
268
|
+
installed = reg.get("installed") or {}
|
|
269
|
+
rec = installed.get(pack_id)
|
|
270
|
+
if not rec:
|
|
271
|
+
marked = _scan_marked_files().get(pack_id)
|
|
272
|
+
if marked: # 记录丢失但标记还在:按标记清理(自愈路径)
|
|
273
|
+
rec = {"file": None, "files": sorted(marked)}
|
|
274
|
+
else:
|
|
275
|
+
return "该包不是市场安装的: %s" % pack_id
|
|
276
|
+
# 只删带本包标记的文件(顶层注入件);标记被摘掉(多半已是用户内容)→
|
|
277
|
+
# 拒绝,防误伤。market-assets/<id>/ 下的附加件由安装独占写入,按位置归属。
|
|
278
|
+
victims = []
|
|
279
|
+
for rel in (rec.get("files") or ([rec["file"]] if rec.get("file") else [])):
|
|
280
|
+
p = _dest_path(pack_id, rel)
|
|
281
|
+
if not p.is_file():
|
|
282
|
+
continue
|
|
283
|
+
toplevel = "/" not in str(rel).replace("\\", "/")
|
|
284
|
+
if toplevel and not _is_market_file(p, pack_id):
|
|
285
|
+
return "文件已不含市场标记,拒绝删除(确要删除请手工处理): %s" % p.name
|
|
286
|
+
victims.append(p)
|
|
287
|
+
for p in victims:
|
|
288
|
+
try:
|
|
289
|
+
p.unlink()
|
|
290
|
+
except OSError as e:
|
|
291
|
+
return "删除失败: %s" % e
|
|
292
|
+
# 随包附加件目录(market-assets/<id>/,安装独占命名空间)整棵清掉
|
|
293
|
+
assets = _user_pack_dir() / "market-assets" / pack_id
|
|
294
|
+
if assets.is_dir():
|
|
295
|
+
shutil.rmtree(assets, ignore_errors=True)
|
|
296
|
+
installed.pop(pack_id, None)
|
|
297
|
+
reg["installed"] = installed
|
|
298
|
+
_save_registry(reg)
|
|
299
|
+
return None
|