codebee 0.1.72 → 0.1.73
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/CHANGELOG.md +6 -0
- package/README.md +4 -4
- package/app/core/flows.py +1 -1
- package/app/core/jobs.py +4 -1
- package/app/core/manager.py +68 -3
- package/app/core/market.py +2 -2
- package/app/core/paihang.py +8 -1
- package/app/core/planner.py +2 -0
- package/app/core/plugins.py +34 -0
- package/app/core/publish/flow.py +54 -3
- package/app/core/publish/manager.py +215 -28
- package/app/core/publish/qimao.py +30 -0
- package/app/core/skillpacks/fanqie-novel.md +66 -48
- package/app/core/skillpacks/qimao-signing.md +70 -59
- package/app/main.py +24 -28
- package/app/ui/app.js +270 -79
- package/app/ui/i18n.js +17 -2
- package/app/ui/index.html +3 -3
- package/app/ui/style.css +7049 -7040
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,8 @@ README 元数据带回,供老版本在「发现新版本」时展示新版更
|
|
|
6
6
|
|
|
7
7
|
## 未发布
|
|
8
8
|
|
|
9
|
+
- **扫榜选材报告逐条带「证据分级」了**:热门题材/高频人设/差异化切入建议每一条末尾标注【证据 A-E】——A=三平台及以上重合、B=两平台重合、C=单平台多次出现、D=单平台仅一次、E=素材无直接证据系推断(借鉴 career-ops 的分级报告交付形态)。此前 AI 选题洞察里的「大热题材」不区分三平台重合的真风口和单平台单次的孤证,读者无从判断该信几分;现在分级只认四源榜单素材的真实出现情况,组合条目按组成元素较低档取值、拿不准就降不升级,报告第一行自带图例。菜单/流程说明与 README 同步。回归:tests/test_rank_scan_grading.py(新增)+ tests/test_rank_scan_copy.py。
|
|
10
|
+
- **作品信息「复制」按钮没用了的根因治了**:作品简介/书名/标签等字段旁的复制按钮点了只弹「已复制」、剪贴板却是空的——数据备份区在文件尾部又定义了一次 `copyText`(读 `data-copy` 属性的元素版),后定义覆盖了前定义,所有传字符串的复制入口(作品信息各字段、对话消息复制、详情页工作目录路径、预览页工作目录)一进函数就 TypeError 静默断掉,而 toast 照弹(async 函数把异常转成无人接的 rejected promise,不阻断后续代码),UI 测试只断言 toast 所以一直全绿。现在:重复定义移除,统一为单一字符串版 `copyText`(剪贴板 API 失败自动回退 execCommand 兜底——局域网 IP/远程地址打开页面时 `navigator.clipboard` 本就不存在,此前这条路也是死的);「已复制」只在真实写进剪贴板后弹,失败明说。回归:tests/ui_bookmeta.mjs 复制断言升级为对账剪贴板真拿到文本(旧断言抓不住这类假成功)。
|
|
9
11
|
- **禅道自动修复失败不再打扰业务流**:修复任务失败时此前会往禅道评论「自动修复未成功」并转派端负责人——失败只是中间态,不是修复结论,写回是拿工具故障打扰业务流,转派更是甩锅(2026-09-29 #27828 实案:评审器没跑起来也照评照转)。现在失败只记台账+群通知,回炉「待重试」状态等下轮扫描自动重试(只重建失败端、成功端不重跑,上限 3 次),到限置「修复失败」终态等人工;禅道页新增「立即处理」按钮,失败/需人工的记录不等下轮扫描随时手工重触发。全程零禅道写入,PUT 清空指派人的老坑彻底不再踩。回归:tests/test_zentao.py 53 项 + tests/ui_zentao.mjs。
|
|
10
12
|
- **提示消息改到屏幕正中**:操作成功/失败的轻提示(toast)此前贴视口底部弹出,在详情页会被底部输入条挡住、经常根本看不见(2026-09-29 实案:「该任务还没跑过,还没有任务详情」整条藏在输入条后面)。现在 toast 在视口水平+垂直正中弹出,仍不拦截鼠标点击,手机窄窗同样居中;无头 Edge 实测居中偏差 ≤4px、窄窗不被拉伸。
|
|
11
13
|
- **「一键升级全部」报 Failed to fetch 的真凶治了(0.1.70 全线写接口劫案)**:0.1.70 起目录页「一键升级全部/单条升级/安装/卸载」一点就报「失败:Failed to fetch」且服务端零痕迹——「已发章数校准」功能在 do_POST 中段路由里写了 `from core import store`,Python 的函数内 import 把 `store` 变成整个 do_POST 的局部名,同函数里所有不经过那行的写路由(新建任务/重试/归档/改名/删除/暂停/取消/清空运行/管理操作全线)一读 `store` 就是 `UnboundLocalError`,socketserver 直接掐断连接、不回任何响应。现在:毒 import 移除(模块顶层本就导入了 store);并给 do_GET/do_POST 加顶层兜底——路由再裸抛统一落服务端日志并回 500 JSON,这类事故从「前端莫名断连+服务端零痕迹」变成一眼可查。回归:tests/e2e_mgmt_upgrade_all.py(本轮 bisect 主力)+ tests/test_http_500_guard.py(500 兜底语义)。
|
|
@@ -36,6 +38,10 @@ README 元数据带回,供老版本在「发现新版本」时展示新版更
|
|
|
36
38
|
- 调度层加固:绑定链上的「纯模型条目」(只配模型名、没挂供应商)起跑前即被点名——所有 CLI 起跑日志新增凭据模式审计行(绑定链注入/无注入/本机默认,密钥值绝不入日志),缺凭据+登录闸门类错误按 CLI 附修复指引;不再只有 claude 有这层观测(2026-09-25 Mac claude 实案的三层加固通用化)。
|
|
37
39
|
- **Mac 桌宠不再是「白板蜜蜂」**:macOS 的 Tk 没有逐像素窗口透明(`-transparentcolor` 是 Windows 专属),蜜蜂的矩形底板整个露出来还带系统边框(实拍=一块白板)。现在非 Windows 平台把底板画成深色卡片(对齐悬停清单面板色),桌宠呈「桌面小组件」形态;睡觉/失联的状态滤镜改为只作用于蜂体(原先整帧压暗靠键色噪点兜底,卡片底兜不回来会浮出一块更暗的小框)。Windows 逐像素透明不受影响。
|
|
38
40
|
|
|
41
|
+
## v0.1.73(2026-09-30)
|
|
42
|
+
|
|
43
|
+
- **建书假「已建书」三修**(我的马甲藏不住实案):建书未取得 book_id 先按书名对账补回、仍空则**不落台账**(空条目曾让界面亮「已建书」);校准遇空 book_id 先对账、找不回明报「可能尚未建成」,不再盲跑作品列表页误报「可能改版」;七猫补齐对账通道(resolve_book_id)
|
|
44
|
+
|
|
39
45
|
## v0.1.72(2026-09-24)
|
|
40
46
|
|
|
41
47
|
- 作品信息沿连载链继承:续写批次与首展示同账,发布绑定/已发章数整链贯通
|
package/README.md
CHANGED
|
@@ -33,10 +33,10 @@ Trae Agent……),提供
|
|
|
33
33
|
---
|
|
34
34
|
|
|
35
35
|
<!-- relnotes:start -->
|
|
36
|
-
### 最新版更新内容(v0.1.
|
|
36
|
+
### 最新版更新内容(v0.1.73)
|
|
37
37
|
|
|
38
|
-
-
|
|
39
|
-
-
|
|
38
|
+
- 建书假「已建书」三修:book_id 空时按书名对账补回、找不回不落台账不亮「已建书」、七猫补齐对账通道
|
|
39
|
+
- 扫榜选材与排名扫描增强(flows/paihang)
|
|
40
40
|
<!-- relnotes:end -->
|
|
41
41
|
|
|
42
42
|
|
|
@@ -252,7 +252,7 @@ python tools\bee.py discard <task> :: 否决——删除任务分支(需输
|
|
|
252
252
|
| 🎬 短视频脚本 | review | 画面 + 口播 + 字幕/音效 → 多维评审 → 修订 → 发布门禁 |
|
|
253
253
|
| 📝 文档 | review | 起草 → 多维评审(准确性/结构/表达/实用)→ 修订 → 门禁 |
|
|
254
254
|
| 🌐 翻译 | review | 起草 → 多维评审(忠实度/流畅度/术语/风格)→ 修订 → 门禁 |
|
|
255
|
-
| 📈 扫榜选材 | direct | 抓七猫/番茄/起点/纵横四平台榜单 →
|
|
255
|
+
| 📈 扫榜选材 | direct | 抓七猫/番茄/起点/纵横四平台榜单 → 跨平台题材/人设/切入点洞察,逐条附 A-E 证据分级 |
|
|
256
256
|
| 🕓 缺陷复盘 | direct | 读禅道/Jira 导出 → 产品/开发/测试三视角复盘报告 |
|
|
257
257
|
| 🔍 调研报告 | review | 起草 → 多维评审(全面性/深度/论据/结论)→ 修订 → 门禁 |
|
|
258
258
|
| 🎤 演讲稿 | review | 起草 → 多维评审(主题/结构/感染力/语言)→ 修订 → 门禁 |
|
package/app/core/flows.py
CHANGED
|
@@ -79,7 +79,7 @@ BUILTIN_FLOWS = [
|
|
|
79
79
|
"note": "起草 → 多维评审 → 修订循环 → 发布门禁"},
|
|
80
80
|
{"id": "rank_scan", "name": "扫榜选材", "icon": "i-chart", "engine": "direct", "builtin": True,
|
|
81
81
|
"goal_hint": "想写哪个方向(一句话,可留空默认分析总榜热门题材)",
|
|
82
|
-
"note": "抓取七猫/番茄/起点/纵横四平台排行榜公开数据 → AI
|
|
82
|
+
"note": "抓取七猫/番茄/起点/纵横四平台排行榜公开数据 → AI 提炼跨平台热门题材/人设/差异化切入点,每条附 A-E 跨平台证据分级(快档直出报告)"},
|
|
83
83
|
{"id": "defect_retro", "name": "缺陷复盘", "icon": "i-history", "engine": "direct", "builtin": True,
|
|
84
84
|
"goal_hint": "复盘哪个版本/时间段的缺陷(建议附带禅道或 Jira 导出 CSV)",
|
|
85
85
|
"note": "读取附带的缺陷导出数据 → AI 输出产品/开发/测试三视角复盘报告:整体画像、根因聚类、漏测分析、改进动作(快档直出报告)"},
|
package/app/core/jobs.py
CHANGED
|
@@ -924,7 +924,10 @@ def _do_mgmt(job, ev):
|
|
|
924
924
|
summary = ("失败:安装文件被占用(可能有同名程序在运行,或杀毒软件正在扫描),"
|
|
925
925
|
"请关闭占用该文件的程序后重试。完整输出见日志。")
|
|
926
926
|
else:
|
|
927
|
-
|
|
927
|
+
# note 优先:良性收口(如 winget「已装无可升级」复检在装)给用户
|
|
928
|
+
# 看原因,别让「完成」两个字孤零零不带解释
|
|
929
|
+
extra = str(res.get("note") or res.get("error") or "")[:300]
|
|
930
|
+
summary = ("完成" if ok else "失败") + (": " + extra if extra else "")
|
|
928
931
|
store.finish_step(run_id, step["n"],
|
|
929
932
|
"done" if ok else "failed",
|
|
930
933
|
summary=summary,
|
package/app/core/manager.py
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"""
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import glob
|
|
9
10
|
import hashlib
|
|
10
11
|
import json
|
|
11
12
|
import os
|
|
@@ -89,10 +90,36 @@ def _safe_config_path(raw):
|
|
|
89
90
|
|
|
90
91
|
# ---------------------------------------------------------------- 检测
|
|
91
92
|
|
|
93
|
+
def _which_cli(cli):
|
|
94
|
+
"""PATH 找 CLI;Windows 上再兜底 winget 便携包两处标准落点。
|
|
95
|
+
|
|
96
|
+
winget 装的 CLI(如 Claude Code)把 exe 放进
|
|
97
|
+
%LOCALAPPDATA%\\Microsoft\\WinGet\\Packages\\<PkgId>_*\\,并把该目录写进
|
|
98
|
+
注册表用户 PATH——注册表更新后已在跑的进程(本服务)不继承,shutil.which
|
|
99
|
+
落空 → 卡片「未安装」假象,点安装又被 winget「已装无可升级」非零退出拒掉
|
|
100
|
+
(2026-09-30 实案)。Links 是便携包符号链接标准目录,Packages 按
|
|
101
|
+
<cli>.exe 通配兜另一形态;两条都只兜 which 落空的场景,零额外开销。"""
|
|
102
|
+
path = shutil.which(cli)
|
|
103
|
+
if path:
|
|
104
|
+
return path
|
|
105
|
+
if os.name != "nt":
|
|
106
|
+
return None
|
|
107
|
+
local = os.environ.get("LOCALAPPDATA")
|
|
108
|
+
if not local:
|
|
109
|
+
return None
|
|
110
|
+
links = os.path.join(local, "Microsoft", "WinGet", "Links", cli + ".exe")
|
|
111
|
+
if os.path.isfile(links):
|
|
112
|
+
return links
|
|
113
|
+
for hit in sorted(glob.glob(os.path.join(
|
|
114
|
+
local, "Microsoft", "WinGet", "Packages", "*", cli + ".exe"))):
|
|
115
|
+
return hit
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
92
119
|
def detect_entry(entry):
|
|
93
120
|
d = entry.get("detect") or {}
|
|
94
121
|
if d.get("cli"):
|
|
95
|
-
path =
|
|
122
|
+
path = _which_cli(d["cli"])
|
|
96
123
|
return {"installed": bool(path), "detail": path or ""}
|
|
97
124
|
if d.get("exe"):
|
|
98
125
|
full = _expand(d["exe"])
|
|
@@ -1907,6 +1934,34 @@ def _open_when_ready(port, url, log_path, timeout=30):
|
|
|
1907
1934
|
|
|
1908
1935
|
# ---------------------------------------------------------------- 安装/升级
|
|
1909
1936
|
|
|
1937
|
+
# winget「包已装且无可用升级」退出码(有符号 -1978335189 / 无符号 2316632107)
|
|
1938
|
+
_WINGET_NOOP_EXIT = 0x8A15002B
|
|
1939
|
+
# 双语结论标记:winget 对已装包重复 install 会转 upgrade,无可升级即打这些话
|
|
1940
|
+
_WINGET_NOOP_MARKERS = ("找不到可用的升级", "没有可用的较新的包版本",
|
|
1941
|
+
"No available upgrade", "No newer package versions")
|
|
1942
|
+
|
|
1943
|
+
|
|
1944
|
+
def _winget_already_ok(cmd, op, res):
|
|
1945
|
+
"""winget install/upgrade 撞「已装且无可升级」时的良性判定。
|
|
1946
|
+
|
|
1947
|
+
该场景命令非零退出,但目标态(已安装)本就达成——按失败走会触发 AI
|
|
1948
|
+
修复白烧一轮诊断(2026-09-30 Claude Code 实案:检测漏装 + 点安装 =
|
|
1949
|
+
假失败 + 假修复)。只认显式「无事可做」的退出码/结论话术,且调用方
|
|
1950
|
+
还要复检 installed 才按成功收口。"""
|
|
1951
|
+
if res.get("ok") or op not in ("install", "upgrade"):
|
|
1952
|
+
return False
|
|
1953
|
+
if not str(cmd or "").strip().lower().startswith("winget"):
|
|
1954
|
+
return False
|
|
1955
|
+
code = res.get("exit_code")
|
|
1956
|
+
try:
|
|
1957
|
+
if code is not None and (int(code) & 0xFFFFFFFF) == _WINGET_NOOP_EXIT:
|
|
1958
|
+
return True
|
|
1959
|
+
except (TypeError, ValueError):
|
|
1960
|
+
pass
|
|
1961
|
+
out = "%s\n%s" % (res.get("stdout") or "", res.get("stderr") or "")
|
|
1962
|
+
return any(m in out for m in _WINGET_NOOP_MARKERS)
|
|
1963
|
+
|
|
1964
|
+
|
|
1910
1965
|
def run_mgmt_command(entry, op, cancel_event=None, log_path=None):
|
|
1911
1966
|
"""执行 install/upgrade/uninstall 命令(由任务执行器异步运行,日志实时落盘)。"""
|
|
1912
1967
|
if op == "uninstall":
|
|
@@ -1923,17 +1978,27 @@ def run_mgmt_command(entry, op, cancel_event=None, log_path=None):
|
|
|
1923
1978
|
% (op, entry["id"])}
|
|
1924
1979
|
res = runner.run_process(shell_cmd=cmd, cwd=str(paths.ROOT),
|
|
1925
1980
|
timeout=1800, cancel_event=cancel_event, log_path=log_path)
|
|
1981
|
+
winget_noop = _winget_already_ok(cmd, op, res)
|
|
1926
1982
|
detect_all(force=True)
|
|
1927
1983
|
with _LOCK:
|
|
1928
1984
|
_STATE["versions"].pop(entry["id"], None)
|
|
1985
|
+
if winget_noop and detect_entry(entry)["installed"]:
|
|
1986
|
+
# winget 说「已装且无可升级」、复检也在装:install 语义(确保在装)已达成
|
|
1987
|
+
refresh_update_async(entry)
|
|
1988
|
+
return {"ok": True, "exit_code": res["exit_code"], "command": cmd,
|
|
1989
|
+
"error": "",
|
|
1990
|
+
"note": "winget 报已安装且无可用升级,复检确认本机在装"}
|
|
1929
1991
|
if res["ok"]:
|
|
1930
1992
|
# 安装/升级成功即作废该条目的更新检查缓存并后台复检:否则 10 分钟 TTL
|
|
1931
1993
|
# 内徽章仍显示「有新版本」,诱导同版本重装(重装易撞 EBUSY 文件锁,
|
|
1932
1994
|
# 2026-09-18 dsh 案)。
|
|
1933
1995
|
refresh_update_async(entry)
|
|
1996
|
+
tail = runner.clean_cli_text(res["stderr"])[-800:]
|
|
1997
|
+
if not tail and not res["ok"]:
|
|
1998
|
+
# winget 把结论打在 stdout(stderr 恒空),只回「退出码 N」零信息量
|
|
1999
|
+
tail = runner.clean_cli_text(res["stdout"])[-800:]
|
|
1934
2000
|
return {"ok": res["ok"], "exit_code": res["exit_code"], "command": cmd,
|
|
1935
|
-
"error": "" if res["ok"] else (
|
|
1936
|
-
or "退出码 %s" % res["exit_code"])}
|
|
2001
|
+
"error": "" if res["ok"] else (tail or "退出码 %s" % res["exit_code"])}
|
|
1937
2002
|
|
|
1938
2003
|
|
|
1939
2004
|
def refresh_update_async(entry):
|
package/app/core/market.py
CHANGED
|
@@ -78,9 +78,9 @@ BUILTIN_PACKS = [
|
|
|
78
78
|
# skills 未来新增内置包时落「未分类」兜底,目录仍然可见、不可安装)
|
|
79
79
|
_BUILTIN_META = {
|
|
80
80
|
"fanqie-novel": {"category": "节奏爽点",
|
|
81
|
-
"desc": "
|
|
81
|
+
"desc": "番茄写作与分段数据诊断:后台指标只作假设,历史更新数字和算法推断不作硬规则。"},
|
|
82
82
|
"qimao-signing": {"category": "流程规范",
|
|
83
|
-
"desc": "
|
|
83
|
+
"desc": "七猫写作经验复盘:开篇事件、人物选择与拒稿证据;历史点位和平台政策不是硬门槛。"},
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
|
package/app/core/paihang.py
CHANGED
|
@@ -116,7 +116,10 @@ def fetch_rank_items(limit=40):
|
|
|
116
116
|
|
|
117
117
|
|
|
118
118
|
def rank_scan_prompt(goal):
|
|
119
|
-
"""组装扫榜选材分析提示词。四源全空返回 None(调用方回落直连提示词)。
|
|
119
|
+
"""组装扫榜选材分析提示词。四源全空返回 None(调用方回落直连提示词)。
|
|
120
|
+
|
|
121
|
+
报告契约含逐条 A-E 跨平台证据分级(借鉴 career-ops 的分级报告交付形态):
|
|
122
|
+
分级只认素材实际出现情况——抑制单源孤证被模型放大成「大热题材」。"""
|
|
120
123
|
blocks, seen = [], set()
|
|
121
124
|
for label, fname in _SOURCES:
|
|
122
125
|
items = [t for t in globals()[fname](30) if t not in seen]
|
|
@@ -131,8 +134,12 @@ def rank_scan_prompt(goal):
|
|
|
131
134
|
"## 榜单元素材\n%s\n\n"
|
|
132
135
|
"## 用户想写的方向\n%s\n\n"
|
|
133
136
|
"## 你的产出(Markdown 报告)\n"
|
|
137
|
+
"报告第一行给图例(一行):「证据分级:A=三平台及以上重合,B=两平台重合,"
|
|
138
|
+
"C=单平台多次出现,D=单平台仅一次,E=素材无直接证据、系推断」。\n"
|
|
134
139
|
"1. **热门题材 Top3**:各自的共同特征与上榜代表书目(跨平台重合的题材单独点出——多平台都热说明是真风口)\n"
|
|
135
140
|
"2. **高频人设/套路总结**:3-5 条,点名反复出现的元素\n"
|
|
136
141
|
"3. **差异化切入建议**:2-3 个,结合用户方向给出「题材+人设」组合与一句话理由\n"
|
|
142
|
+
"以上每一项结论末尾标注【证据X】(X 为 A-E 之一)。分级只看素材里真实出现的"
|
|
143
|
+
"情况:组合条目按其组成元素的较低档取值,拿不准就降一档、不升级。\n"
|
|
137
144
|
"直接输出分析报告,不要复述素材清单,不要输出与报告无关的内容。"
|
|
138
145
|
% (material, goal or "(用户未指定方向,按大盘热门分析)"))
|
package/app/core/planner.py
CHANGED
|
@@ -256,6 +256,8 @@ __SKILLS__
|
|
|
256
256
|
只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
|
|
257
257
|
{"book_title": "书名", "style_anchor": "全书文风锚(80字内,具体可执行:叙述视角与人称、句长基调、语域、对白风格、禁用表达)", "chapters": [{"title": "章节标题", "beats": "本章剧情要点(50-120字:事件/冲突/推进)", "hook": "章末钩子(一句话)", "highlight": "本章爽点/情绪爆点(一句话:谁在哪里完成什么反转或释放什么情绪)"}]__VOLUMES_KEY__}
|
|
258
258
|
硬性要求:
|
|
259
|
+
- book_title 4-15 字,必须考虑平台书名全站唯一(撞名建不了书):避开「我的XX藏不住」
|
|
260
|
+
「开局XX」等高频套路组合,书名里带本书独有的专名(人名/金手指/设定词),独有专名越多越不易撞名;
|
|
259
261
|
- 第 1-3 章是黄金三章:第 1 章前 100 字出现主角、前 200 字出现异常或欲望、前 600 字进入具体困境,
|
|
260
262
|
第 3 章末留大钩子;不要用世界观或环境描写拖延切入;
|
|
261
263
|
- 每章都写清“主角主动选择 → 阻碍 → 局面变化 → 章末钩子”,禁止水字数的日常流水账和硬旁白推剧情;
|
package/app/core/plugins.py
CHANGED
|
@@ -9,6 +9,7 @@ bundles are never executed by this module.
|
|
|
9
9
|
"""
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import hashlib
|
|
12
13
|
import json
|
|
13
14
|
import os
|
|
14
15
|
import re
|
|
@@ -523,3 +524,36 @@ def active_mcp_servers():
|
|
|
523
524
|
continue
|
|
524
525
|
return [x for x in out if isinstance(x, dict) and x.get("command")]
|
|
525
526
|
|
|
527
|
+
|
|
528
|
+
def merged_servers_text(user_text):
|
|
529
|
+
"""用户显式 MCP 清单(JSON 文本)+ 已启用插件的声明 → 合成后的 JSON 文本。
|
|
530
|
+
|
|
531
|
+
插件服务器名加 ``plg_`` 前缀+短哈希隔离,防与用户配置撞名;用户清单
|
|
532
|
+
非法或非数组时原样返回,坏插件绝不连累用户显式配置的服务器。
|
|
533
|
+
逻辑原先长在 main() 启动闭包里、测试够不着(ae9bcb8 半交付时两个
|
|
534
|
+
用例即因此恒错),2026-09-30 随 /api/plugins 路由补齐一并下沉。"""
|
|
535
|
+
raw = str(user_text or "")
|
|
536
|
+
try:
|
|
537
|
+
configured = json.loads(raw) if raw.strip() else []
|
|
538
|
+
except (ValueError, TypeError):
|
|
539
|
+
return raw
|
|
540
|
+
if not isinstance(configured, list):
|
|
541
|
+
return raw
|
|
542
|
+
try:
|
|
543
|
+
plugin_servers = active_mcp_servers()
|
|
544
|
+
except Exception:
|
|
545
|
+
plugin_servers = []
|
|
546
|
+
for item in plugin_servers or []:
|
|
547
|
+
if not isinstance(item, dict):
|
|
548
|
+
continue
|
|
549
|
+
plugin_id = str(item.pop("plugin_id", "plugin"))
|
|
550
|
+
server_name = str(item.get("name") or "server")
|
|
551
|
+
digest = hashlib.sha256(
|
|
552
|
+
(plugin_id + ":" + server_name).encode("utf-8")).hexdigest()[:6]
|
|
553
|
+
safe = re.sub(r"[^a-z0-9_-]", "-", plugin_id.lower())
|
|
554
|
+
item["name"] = ("plg_" + safe[:11] + "_" + digest)[:24]
|
|
555
|
+
if not any(x.get("name") == item["name"] for x in configured
|
|
556
|
+
if isinstance(x, dict)):
|
|
557
|
+
configured.append(item)
|
|
558
|
+
return json.dumps(configured, ensure_ascii=False)
|
|
559
|
+
|
package/app/core/publish/flow.py
CHANGED
|
@@ -32,6 +32,47 @@ class FlowError(Exception):
|
|
|
32
32
|
"""流程失败:message 面向用户(含步骤序号与截图路径线索)。"""
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
class TitleDupError(FlowError):
|
|
36
|
+
"""平台拒绝书名:重名(全平台书名唯一)。message 带平台提示原文。
|
|
37
|
+
|
|
38
|
+
与普通流程失败分开成独立类型:manager 建书要按它触发「对账复用 →
|
|
39
|
+
自动换名重试」,普通失败不能换名(换了也建不成)。"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, hint):
|
|
42
|
+
super().__init__("平台拒绝书名(已存在同名作品):%s" % hint)
|
|
43
|
+
self.hint = hint
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# 重名提示识别词(番茄/七猫建书页校验文案口径:「书名已存在」「该名称已被
|
|
47
|
+
# 使用」等)。核心词「已存在/已被使用/已被注册/重名」足够特异——建书页上
|
|
48
|
+
# 出现这些词几乎必然指向书名撞车;不加宽泛的「重复」,防误伤其它字段校验。
|
|
49
|
+
DUP_HINT_WORDS = ("已存在", "已被使用", "已被注册", "重名", "已被占用")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _dup_probe_js():
|
|
53
|
+
# 抓页面上可见提示里命中重名关键词的那条(toast/表单校验/弹窗)。
|
|
54
|
+
# 选择器与 _page_error_text_js 同源;只回命中词的那条,空串=页面没喊重名。
|
|
55
|
+
return ("(words)=>{const sels='.arco-message,.arco-notification,"
|
|
56
|
+
"[class*=message],[class*=toast],[class*=error],[class*=alert],"
|
|
57
|
+
"[class*=tip],[class*=tooltip],[class*=valid]';"
|
|
58
|
+
"for(const e of document.querySelectorAll(sels)){"
|
|
59
|
+
"const r=e.getBoundingClientRect();"
|
|
60
|
+
"if(r.width<=0||r.height<=0)continue;"
|
|
61
|
+
"const x=(e.innerText||'').trim();"
|
|
62
|
+
"if(!x||x.length>160)continue;"
|
|
63
|
+
"for(const w of words){if(x.includes(w))return x;}}"
|
|
64
|
+
"return '';}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def dup_hint(page, timeout=5):
|
|
68
|
+
"""读页面上的重名提示文本;无/读失败返回空串。预检与事后探测共用。"""
|
|
69
|
+
try:
|
|
70
|
+
v = page.call(_dup_probe_js(), list(DUP_HINT_WORDS), timeout=timeout)
|
|
71
|
+
return v.strip() if isinstance(v, str) else ""
|
|
72
|
+
except Exception:
|
|
73
|
+
return ""
|
|
74
|
+
|
|
75
|
+
|
|
35
76
|
def _click_match_js():
|
|
36
77
|
# 点同时包含所有关键词的最小可见元素(长度最小者=最内层卡片)
|
|
37
78
|
return ("(keys,maxLen)=>{"
|
|
@@ -212,11 +253,14 @@ def _tags_select_grouped(page, note, i, pairs):
|
|
|
212
253
|
|
|
213
254
|
|
|
214
255
|
def run_flow(page, steps, values=None, config=None, auto_submit=False,
|
|
215
|
-
shot=None, log=None):
|
|
256
|
+
shot=None, log=None, dup_check=False):
|
|
216
257
|
"""跑一个流程。values:fill 取值字典;config:平台 URL 等占位符来源。
|
|
217
258
|
|
|
218
259
|
shot(name) → 截图落盘函数(manager 注入,路径含任务/平台维度);
|
|
219
|
-
log(line) → 步骤日志函数(进发布记录的 log 字段)。返回执行到的步数。
|
|
260
|
+
log(line) → 步骤日志函数(进发布记录的 log 字段)。返回执行到的步数。
|
|
261
|
+
dup_check → 提交前探测页面重名提示(建书专用):平台实时校验喊「已
|
|
262
|
+
存在」时点提交必然被拒、流程只会误报「未登录或改版」——在点击前抛
|
|
263
|
+
TitleDupError 让上层走换名重试,别白点(2026-09-30 马甲案)。"""
|
|
220
264
|
values = values or {}
|
|
221
265
|
config = config or {}
|
|
222
266
|
log = log or (lambda s: None)
|
|
@@ -440,7 +484,7 @@ def run_flow(page, steps, values=None, config=None, auto_submit=False,
|
|
|
440
484
|
if sections:
|
|
441
485
|
break
|
|
442
486
|
time.sleep(1.0)
|
|
443
|
-
if sections:
|
|
487
|
+
if sections and isinstance(sections, list):
|
|
444
488
|
missing = {g for g, _ in pairs} - {s["g"] for s in sections}
|
|
445
489
|
if missing:
|
|
446
490
|
# 弹层是分组结构但组名对不上=平台目录/组名改了——
|
|
@@ -495,6 +539,13 @@ def run_flow(page, steps, values=None, config=None, auto_submit=False,
|
|
|
495
539
|
if shot:
|
|
496
540
|
shot("ready-manual-submit")
|
|
497
541
|
return i + 1
|
|
542
|
+
if dup_check:
|
|
543
|
+
# 事前预检:填表阶段平台实时校验(fill 派发过 change 事件,
|
|
544
|
+
# blur/输入即触发)喊了重名就别点了,点了也必被拒
|
|
545
|
+
hint = dup_hint(page)
|
|
546
|
+
if hint:
|
|
547
|
+
_fail_shot(shot, "step%d-title-dup" % i)
|
|
548
|
+
raise TitleDupError(hint)
|
|
498
549
|
if st.get("text"): # 按按钮文本提交
|
|
499
550
|
text = str(st["text"])
|
|
500
551
|
note(i, "提交「%s」(真实鼠标事件)" % text)
|