codebee 0.1.101 → 0.1.103
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 +14 -0
- package/LICENSE +21 -21
- package/README.md +3 -6
- package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
- package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
- package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
- package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
- package/app/.playwright-cli/run-detail-after.png +0 -0
- package/app/core/acceptance.py +100 -100
- package/app/core/acp_client.py +457 -426
- package/app/core/aiflavor.py +197 -197
- package/app/core/analytics.py +38 -38
- package/app/core/artifacts.py +148 -148
- package/app/core/assets.py +110 -110
- package/app/core/attachments.py +623 -623
- package/app/core/audit.py +30 -30
- package/app/core/backend_profiles.py +188 -188
- package/app/core/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/benchstore.py +102 -102
- package/app/core/board.py +21 -17
- package/app/core/bookmeta.py +1036 -1036
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +13 -4
- package/app/core/capability.py +144 -144
- package/app/core/catalog.py +28 -6
- package/app/core/chaptersafe.py +32 -32
- package/app/core/checkpoints.py +117 -31
- package/app/core/cleanup.py +303 -303
- package/app/core/command_auth.py +302 -302
- package/app/core/compaction.py +201 -201
- package/app/core/contracts.py +255 -255
- package/app/core/covergen.py +356 -356
- package/app/core/defectretro.py +43 -43
- package/app/core/diagnostics.py +114 -114
- package/app/core/dispatch.py +1 -1
- package/app/core/dispatch_log.py +128 -128
- package/app/core/env_scrub.py +85 -85
- package/app/core/errorlog.py +137 -137
- package/app/core/eval_matrix.py +91 -82
- package/app/core/evaluation.py +174 -174
- package/app/core/failure_notify.py +290 -290
- package/app/core/flow_graph.py +33 -33
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +389 -389
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +162 -162
- package/app/core/knowledge_pipeline.py +86 -86
- package/app/core/manager.py +2385 -2385
- package/app/core/mcp_client.py +7 -2
- package/app/core/mocks.py +64 -64
- package/app/core/notify.py +201 -201
- package/app/core/novel_quality.py +181 -181
- package/app/core/openapi.py +217 -209
- package/app/core/operations.py +228 -228
- package/app/core/paihang.py +145 -145
- package/app/core/pipeline.py +6 -3
- package/app/core/policy.py +9 -0
- package/app/core/portguard.py +111 -111
- package/app/core/presence.py +77 -77
- package/app/core/preview.py +293 -293
- package/app/core/project_memory.py +206 -206
- package/app/core/prompt_guard.py +78 -78
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/auto.py +659 -562
- package/app/core/publish/chaprows.py +138 -138
- package/app/core/publish/fanqie.py +16 -10
- package/app/core/publish/flow.py +29 -1
- package/app/core/publish/flows-fanqie-calibrated.json +442 -442
- package/app/core/publish/manager.py +1185 -1177
- package/app/core/publish/qimao.py +193 -193
- package/app/core/publish/volumes.py +108 -108
- package/app/core/publish/ws.py +139 -139
- package/app/core/quality_gate.py +201 -201
- package/app/core/redact.py +38 -38
- package/app/core/repeat_guard.py +129 -129
- package/app/core/retrieval.py +137 -137
- package/app/core/sandbox_runtime.py +95 -0
- package/app/core/selfupdate.py +384 -384
- package/app/core/session_log.py +161 -161
- package/app/core/sessions.py +312 -312
- package/app/core/settings.py +215 -215
- package/app/core/settings_schema.py +282 -282
- package/app/core/share_page.py +114 -114
- package/app/core/skill_scan.py +139 -139
- package/app/core/skillpacks/market/character-bible.md +66 -66
- package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
- package/app/core/skillpacks/market/git-workflow.md +57 -57
- package/app/core/skillpacks/market/release-notes.md +72 -72
- package/app/core/skillpacks/market/weekly-report.md +71 -71
- package/app/core/skillpacks/market/worldview-consistency.md +70 -70
- package/app/core/step_runner.py +132 -132
- package/app/core/store.py +13 -3
- package/app/core/story_tracking.py +275 -275
- package/app/core/task_compile.py +199 -199
- package/app/core/telemetry.py +292 -292
- package/app/core/tlsctx.py +64 -64
- package/app/core/token_meter.py +147 -147
- package/app/core/tool_outputs.py +107 -107
- package/app/core/tracing.py +207 -207
- package/app/core/upstream.py +41 -41
- package/app/core/usage.py +1038 -1038
- package/app/core/volumes.py +389 -389
- package/app/core/webhooks.py +89 -89
- package/app/core/wxdigest.py +865 -865
- package/app/core/zentao.py +2692 -2692
- package/app/main.py +1 -0
- package/app/mcp_server.py +244 -244
- package/app/output/_createbook_body.json +1 -0
- package/app/output/_probe_fq_click.py +50 -0
- package/app/output/_probe_fq_form.py +42 -0
- package/app/output/_probe_fq_modal.py +55 -0
- package/app/output/_probe_fq_points.py +71 -0
- package/app/output/_probe_fq_submit.py +34 -0
- package/app/output/_probe_fq_watch.py +62 -0
- package/app/output/workbench-clean-preview.jpg +0 -0
- package/app/output/workbench-reference-render.png +0 -0
- package/app/output/workbench-shell.png +0 -0
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +165 -29
- package/app/ui/artdiff.js +74 -74
- package/app/ui/hive3d.js +491 -223
- package/app/ui/i18n.js +18 -1
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +24 -4
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +548 -18
- package/bin/tutti.js +147 -147
- package/package.json +1 -1
- package/tools/wx_reader.py +261 -261
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""番茄建书分类弹层实地探测:复现点击序列 → dump 弹层真实结构。只读不提交。"""
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, r"E:\GoOut\MultiAgentOrchestration")
|
|
8
|
+
from app.core.publish import manager # noqa: E402
|
|
9
|
+
|
|
10
|
+
b, page = manager._open_page("fanqie")
|
|
11
|
+
# 硬刷新拿干净表单(同 URL 导航 SPA 不重置,上次建书残留会干扰)
|
|
12
|
+
page.navigate("https://fanqienovel.com/main/writer/create", timeout=30)
|
|
13
|
+
page.call("() => location.reload()")
|
|
14
|
+
time.sleep(5)
|
|
15
|
+
|
|
16
|
+
CLICK_JS = """(t) => {
|
|
17
|
+
const els = [...document.querySelectorAll('span,div,button,label')]
|
|
18
|
+
.filter(e => e.offsetParent && e.textContent.trim() === t);
|
|
19
|
+
if (!els.length) return {ok: false, why: 'no ' + t};
|
|
20
|
+
els[els.length - 1].click();
|
|
21
|
+
return {ok: true, n: els.length};
|
|
22
|
+
}"""
|
|
23
|
+
|
|
24
|
+
for text in ["连载模式", "男频"]:
|
|
25
|
+
r = page.call(CLICK_JS, [text])
|
|
26
|
+
print(text, "->", r)
|
|
27
|
+
time.sleep(1.5)
|
|
28
|
+
|
|
29
|
+
time.sleep(1)
|
|
30
|
+
r = page.call("""() => {
|
|
31
|
+
const els = [...document.querySelectorAll('span,div,button,label')]
|
|
32
|
+
.filter(e => e.offsetParent && e.textContent.includes('请选择作品标签'));
|
|
33
|
+
if (!els.length) return {ok: false, why: 'no tag entry'};
|
|
34
|
+
els[els.length - 1].click();
|
|
35
|
+
return {ok: true, n: els.length};
|
|
36
|
+
}""")
|
|
37
|
+
print("请选择作品标签 ->", r)
|
|
38
|
+
time.sleep(3)
|
|
39
|
+
|
|
40
|
+
DUMP_JS = """() => {
|
|
41
|
+
const cm = document.querySelector('.category-modal');
|
|
42
|
+
const out = {category_modal_exists: !!cm, cm_class: cm ? String(cm.className) : ''};
|
|
43
|
+
const dialogs = [...document.querySelectorAll(
|
|
44
|
+
'[class*=modal],[class*=dialog],[class*=popover],[class*=picker],[class*=drawer],[class*=popup]')]
|
|
45
|
+
.filter(e => e.offsetParent).slice(0, 8);
|
|
46
|
+
out.dialogs = dialogs.map(d => ({
|
|
47
|
+
cls: String(d.className).slice(0, 100),
|
|
48
|
+
text: (d.innerText || '').slice(0, 500).split('\\n').join('|')
|
|
49
|
+
}));
|
|
50
|
+
out.has_dushi = document.body.innerText.indexOf('都市脑洞') >= 0;
|
|
51
|
+
out.body_snip = document.body.innerText.slice(0, 600).split('\\n').join('|');
|
|
52
|
+
return out;
|
|
53
|
+
}"""
|
|
54
|
+
info = page.call(DUMP_JS)
|
|
55
|
+
print(json.dumps(info, ensure_ascii=False, indent=1))
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""逐点试探「请选择作品标签」:不同落点/事件序列,找出能打开分类弹层的精确触发方式。只读。"""
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, r"E:\GoOut\MultiAgentOrchestration")
|
|
8
|
+
from app.core.publish import manager # noqa: E402
|
|
9
|
+
|
|
10
|
+
b, page = manager._open_page("fanqie")
|
|
11
|
+
page.navigate("https://fanqienovel.com/main/writer/create", timeout=30)
|
|
12
|
+
page.call("() => location.reload()")
|
|
13
|
+
time.sleep(5)
|
|
14
|
+
|
|
15
|
+
# 前置:先把签约模式/目标读者点上(复现产品完整前置链)
|
|
16
|
+
for text in ["连载模式", "男频"]:
|
|
17
|
+
r = page.real_click_text(text, "label.arco-radio,span.arco-radio,[class*=serial-radio]",
|
|
18
|
+
y_max=430)
|
|
19
|
+
print("前置点击", text, "->", r)
|
|
20
|
+
time.sleep(1.0)
|
|
21
|
+
|
|
22
|
+
MODAL_JS = "() => !!document.querySelector('.category-modal')"
|
|
23
|
+
|
|
24
|
+
# 找到 select-row 及其内部结构
|
|
25
|
+
r = page.call("""() => {
|
|
26
|
+
const row = [...document.querySelectorAll('.select-row')]
|
|
27
|
+
.find(e => (e.innerText || '').includes('请选择作品标签'));
|
|
28
|
+
if (!row) return {ok: false};
|
|
29
|
+
const inner = [...row.querySelectorAll('*')].slice(0, 8).map(e => {
|
|
30
|
+
const rc = e.getBoundingClientRect();
|
|
31
|
+
return {tag: e.tagName, cls: String(e.className).slice(0, 50),
|
|
32
|
+
x: Math.round(rc.x + rc.width / 2), y: Math.round(rc.y + rc.height / 2)};
|
|
33
|
+
});
|
|
34
|
+
const rc = row.getBoundingClientRect();
|
|
35
|
+
return {ok: true, row: {x: Math.round(rc.x + rc.width / 2), y: Math.round(rc.y + rc.height / 2)},
|
|
36
|
+
inner: inner};
|
|
37
|
+
}""")
|
|
38
|
+
print("select-row 结构:", json.dumps(r, ensure_ascii=False))
|
|
39
|
+
if not r.get("ok"):
|
|
40
|
+
sys.exit("select-row 没找到")
|
|
41
|
+
|
|
42
|
+
targets = [r["row"]] + r["inner"]
|
|
43
|
+
for t in targets:
|
|
44
|
+
x, y = t["x"], t["y"]
|
|
45
|
+
page.send("Input.dispatchMouseEvent",
|
|
46
|
+
{"type": "mousePressed", "x": x, "y": y, "button": "left", "clickCount": 1},
|
|
47
|
+
timeout=8.0)
|
|
48
|
+
page.send("Input.dispatchMouseEvent",
|
|
49
|
+
{"type": "mouseReleased", "x": x, "y": y, "button": "left", "clickCount": 1},
|
|
50
|
+
timeout=8.0)
|
|
51
|
+
time.sleep(1.8)
|
|
52
|
+
opened = page.call(MODAL_JS)
|
|
53
|
+
print("点 %s(%s) @(%d,%d) -> 弹层: %s" % (t.get("tag"), t.get("cls", "")[:30], x, y, opened))
|
|
54
|
+
if opened:
|
|
55
|
+
print(">>> 弹层开了!落点:", json.dumps(t, ensure_ascii=False))
|
|
56
|
+
# dump 弹层内主分类区
|
|
57
|
+
info = page.call("""() => {
|
|
58
|
+
const cm = document.querySelector('.category-modal');
|
|
59
|
+
return {cls: String(cm.className).slice(0, 80),
|
|
60
|
+
txt: (cm.innerText || '').slice(0, 300).split('\\n').join('|')};
|
|
61
|
+
}""")
|
|
62
|
+
print(json.dumps(info, ensure_ascii=False, indent=1))
|
|
63
|
+
break
|
|
64
|
+
# 收起可能的意外浮层,回干净状态再试下一个点
|
|
65
|
+
page.send("Input.dispatchMouseEvent",
|
|
66
|
+
{"type": "mousePressed", "x": 200, "y": 700, "button": "left", "clickCount": 1},
|
|
67
|
+
timeout=8.0)
|
|
68
|
+
page.send("Input.dispatchMouseEvent",
|
|
69
|
+
{"type": "mouseReleased", "x": 200, "y": 700, "button": "left", "clickCount": 1},
|
|
70
|
+
timeout=8.0)
|
|
71
|
+
time.sleep(0.8)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""实地补点「立即创建」并全程盯提交反应(弹窗/跳转)。真实提交,已获用户批准。"""
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, r"E:\GoOut\MultiAgentOrchestration")
|
|
8
|
+
from app.core.publish import manager # noqa: E402
|
|
9
|
+
|
|
10
|
+
b, page = manager._open_page("fanqie")
|
|
11
|
+
print("url before:", page.url())
|
|
12
|
+
|
|
13
|
+
r = page.real_click_text("立即创建", "button", contains=False)
|
|
14
|
+
print("点「立即创建」->", r)
|
|
15
|
+
|
|
16
|
+
for i in range(12):
|
|
17
|
+
time.sleep(2.5)
|
|
18
|
+
url = str(page.url() or "")
|
|
19
|
+
st = page.call("""() => {
|
|
20
|
+
const dlg = [...document.querySelectorAll('.arco-modal,[class*=dialog],[class*=modal]')]
|
|
21
|
+
.filter(e => e.offsetParent && (e.innerText || '').trim())
|
|
22
|
+
.slice(0, 3)
|
|
23
|
+
.map(e => ({cls: String(e.className).slice(0, 50),
|
|
24
|
+
txt: (e.innerText || '').slice(0, 200).split('\\n').join('|')}));
|
|
25
|
+
return {dlg: dlg,
|
|
26
|
+
toast: ([...document.querySelectorAll('[class*=message],[class*=toast]')]
|
|
27
|
+
.filter(e => e.offsetParent).map(e => (e.innerText || '').slice(0, 80)) )};
|
|
28
|
+
}""")
|
|
29
|
+
print("[%02d] url=%s" % (i, url[:90]))
|
|
30
|
+
if st.get("dlg") or st.get("toast"):
|
|
31
|
+
print(" 弹窗/提示:", json.dumps(st, ensure_ascii=False)[:400])
|
|
32
|
+
if "book-info" in url or "chapter" in url:
|
|
33
|
+
print(">>> 建书成功跳转!")
|
|
34
|
+
break
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""建书全程盯梢:触发建书线程的同时高频 dump 页面弹窗/toast/URL,抓提交瞬间反应。"""
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
import json
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, r"E:\GoOut\MultiAgentOrchestration")
|
|
9
|
+
|
|
10
|
+
BODY = r"E:\GoOut\MultiAgentOrchestration\app\output\_createbook_body.json"
|
|
11
|
+
|
|
12
|
+
def trigger():
|
|
13
|
+
import http.client
|
|
14
|
+
body = open(BODY, "rb").read()
|
|
15
|
+
conn = http.client.HTTPConnection("127.0.0.1", 8765, timeout=30)
|
|
16
|
+
conn.request("POST", "/api/control",
|
|
17
|
+
json.dumps({"action": "acquire", "force": True}).encode(),
|
|
18
|
+
{"Content-Type": "application/json", "X-CodeBee-Client": "cli-maint-0930"})
|
|
19
|
+
conn.getresponse().read()
|
|
20
|
+
conn.request("POST", "/api/publish/task/t-20260924-215431-0715/create-book",
|
|
21
|
+
body, {"Content-Type": "application/json",
|
|
22
|
+
"X-CodeBee-Client": "cli-maint-0930"})
|
|
23
|
+
print("trigger:", conn.getresponse().read()[:120])
|
|
24
|
+
conn.request("POST", "/api/control",
|
|
25
|
+
json.dumps({"action": "release"}).encode(),
|
|
26
|
+
{"Content-Type": "application/json", "X-CodeBee-Client": "cli-maint-0930"})
|
|
27
|
+
conn.getresponse().read()
|
|
28
|
+
|
|
29
|
+
threading.Thread(target=trigger, daemon=True).start()
|
|
30
|
+
time.sleep(2)
|
|
31
|
+
|
|
32
|
+
from app.core.publish import manager # noqa: E402
|
|
33
|
+
|
|
34
|
+
last = ""
|
|
35
|
+
for i in range(75):
|
|
36
|
+
try:
|
|
37
|
+
b, page = manager._open_page("fanqie")
|
|
38
|
+
url = str(page.url() or "")
|
|
39
|
+
st = page.call("""() => {
|
|
40
|
+
const grab = sel => [...document.querySelectorAll(sel)]
|
|
41
|
+
.filter(e => e.offsetParent && (e.innerText || '').trim())
|
|
42
|
+
.map(e => (e.innerText || '').trim().slice(0, 100));
|
|
43
|
+
return {
|
|
44
|
+
dlg: grab('.arco-modal'),
|
|
45
|
+
toast: grab('[class*=message],[class*=toast],[class*=notification]'),
|
|
46
|
+
btn: grab('button').filter(t => t.length < 12).slice(0, 10)
|
|
47
|
+
};
|
|
48
|
+
}""") or {}
|
|
49
|
+
sig = json.dumps([url, st.get("dlg"), st.get("toast")], ensure_ascii=False)
|
|
50
|
+
if sig != last:
|
|
51
|
+
last = sig
|
|
52
|
+
print("[%03d] %s" % (i, url[:80]))
|
|
53
|
+
if st.get("dlg"):
|
|
54
|
+
print(" 弹窗:", json.dumps(st["dlg"], ensure_ascii=False)[:300])
|
|
55
|
+
if st.get("toast"):
|
|
56
|
+
print(" toast:", json.dumps(st["toast"], ensure_ascii=False)[:300])
|
|
57
|
+
if "book-info" in url:
|
|
58
|
+
print(">>> 跳转 book-info!")
|
|
59
|
+
break
|
|
60
|
+
except Exception as e:
|
|
61
|
+
print("[%03d] watch err: %s" % (i, str(e)[:80]))
|
|
62
|
+
time.sleep(2)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/app/pick_dialog.py
CHANGED
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""原生「选择文件夹」对话框(main.py 的 /api/pick_folder 用)。
|
|
3
|
-
|
|
4
|
-
Tk 必须活在自家进程的主线程里:HTTP 请求线程里建 root 在 macOS 上会崩,
|
|
5
|
-
Windows 上反复建/销毁也不稳,所以隔离成独立进程。ask_directory() 是父端
|
|
6
|
-
入口,把本文件拉起为子进程,请求参数(JSON:initial 起始目录 / title 标题)
|
|
7
|
-
走 stdin,选中目录以 JSON({"path": ...})走 stdout,用户取消回空串;
|
|
8
|
-
对话框部分是文件尾的 main()。任何一步失败都以非零码退出,父端据此回
|
|
9
|
-
fallback=true 让前端回落网页目录弹框。"""
|
|
10
|
-
import json
|
|
11
|
-
import os
|
|
12
|
-
import subprocess
|
|
13
|
-
import sys
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
|
|
16
|
-
# 非 Windows 置 0:POSIX 的 Popen 对非零 creationflags 抛 ValueError(manager 同款守卫)
|
|
17
|
-
CREATE_NO_WINDOW = 0x08000000 if os.name == "nt" else 0
|
|
18
|
-
_SELF = str(Path(__file__).resolve())
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def ask_directory(initial="", title="选择文件夹"):
|
|
22
|
-
"""父端入口:拉起子进程弹原生对话框,返回 (path, error, fallback)。
|
|
23
|
-
|
|
24
|
-
用户取消时 path 为空串;子进程起不来(机器无 tkinter 等)时 error
|
|
25
|
-
非空且 fallback=True,调用方原样转给前端回落网页目录弹框。"""
|
|
26
|
-
if not Path(__file__).exists():
|
|
27
|
-
return "", "pick_dialog.py 缺失", True
|
|
28
|
-
req = json.dumps({"initial": initial, "title": title}).encode("utf-8")
|
|
29
|
-
env = dict(os.environ, PYTHONIOENCODING="utf-8")
|
|
30
|
-
try:
|
|
31
|
-
if os.name == "nt":
|
|
32
|
-
cp = subprocess.run([sys.executable, _SELF], input=req,
|
|
33
|
-
capture_output=True, timeout=600, env=env,
|
|
34
|
-
creationflags=CREATE_NO_WINDOW)
|
|
35
|
-
else:
|
|
36
|
-
cp = subprocess.run([sys.executable, _SELF], input=req,
|
|
37
|
-
capture_output=True, timeout=600, env=env)
|
|
38
|
-
except Exception as e:
|
|
39
|
-
return "", str(e), True
|
|
40
|
-
if cp.returncode != 0:
|
|
41
|
-
err = (cp.stderr or b"").decode("utf-8", "replace").strip().splitlines()
|
|
42
|
-
return "", (err[-1] if err else "native picker unavailable"), True
|
|
43
|
-
try:
|
|
44
|
-
data = json.loads((cp.stdout or b"").decode("utf-8", "replace") or "{}")
|
|
45
|
-
except Exception:
|
|
46
|
-
return "", "对话框输出无法解析", True
|
|
47
|
-
return str(data.get("path") or ""), "", False
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def ask_file(initial="", title="选择文件"):
|
|
51
|
-
"""父端入口:拉起子进程弹原生「选择文件」对话框(导入备份包用)。
|
|
52
|
-
返回 (path, error, fallback),语义与 ask_directory 一致。"""
|
|
53
|
-
if not Path(__file__).exists():
|
|
54
|
-
return "", "pick_dialog.py 缺失", True
|
|
55
|
-
req = json.dumps({"mode": "file", "initial": initial, "title": title}).encode("utf-8")
|
|
56
|
-
env = dict(os.environ, PYTHONIOENCODING="utf-8")
|
|
57
|
-
try:
|
|
58
|
-
if os.name == "nt":
|
|
59
|
-
cp = subprocess.run([sys.executable, _SELF], input=req,
|
|
60
|
-
capture_output=True, timeout=600, env=env,
|
|
61
|
-
creationflags=CREATE_NO_WINDOW)
|
|
62
|
-
else:
|
|
63
|
-
cp = subprocess.run([sys.executable, _SELF], input=req,
|
|
64
|
-
capture_output=True, timeout=600, env=env)
|
|
65
|
-
except Exception as e:
|
|
66
|
-
return "", str(e), True
|
|
67
|
-
if cp.returncode != 0:
|
|
68
|
-
err = (cp.stderr or b"").decode("utf-8", "replace").strip().splitlines()
|
|
69
|
-
return "", (err[-1] if err else "native picker unavailable"), True
|
|
70
|
-
try:
|
|
71
|
-
data = json.loads((cp.stdout or b"").decode("utf-8", "replace") or "{}")
|
|
72
|
-
except Exception:
|
|
73
|
-
return "", "对话框输出无法解析", True
|
|
74
|
-
return str(data.get("path") or ""), "", False
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def main():
|
|
78
|
-
req = {}
|
|
79
|
-
try:
|
|
80
|
-
req = json.loads(sys.stdin.read() or "{}")
|
|
81
|
-
except Exception:
|
|
82
|
-
pass
|
|
83
|
-
import tkinter as tk
|
|
84
|
-
from tkinter import filedialog
|
|
85
|
-
|
|
86
|
-
root = tk.Tk()
|
|
87
|
-
root.withdraw()
|
|
88
|
-
try:
|
|
89
|
-
root.attributes("-topmost", True) # 别被全屏浏览器盖住
|
|
90
|
-
except Exception:
|
|
91
|
-
pass
|
|
92
|
-
is_file = req.get("mode") == "file"
|
|
93
|
-
kw = {"title": req.get("title") or ("选择文件" if is_file else "选择文件夹")}
|
|
94
|
-
init = req.get("initial") or ""
|
|
95
|
-
if init and os.path.isdir(init):
|
|
96
|
-
kw["initialdir"] = init
|
|
97
|
-
try:
|
|
98
|
-
if is_file:
|
|
99
|
-
kw["filetypes"] = [("备份包", "*.zip"), ("全部文件", "*.*")]
|
|
100
|
-
path = filedialog.askopenfilename(**kw) or ""
|
|
101
|
-
else:
|
|
102
|
-
path = filedialog.askdirectory(**kw) or ""
|
|
103
|
-
except Exception:
|
|
104
|
-
sys.exit(1)
|
|
105
|
-
sys.stdout.write(json.dumps({"path": path}))
|
|
106
|
-
root.destroy()
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if __name__ == "__main__":
|
|
110
|
-
main()
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""原生「选择文件夹」对话框(main.py 的 /api/pick_folder 用)。
|
|
3
|
+
|
|
4
|
+
Tk 必须活在自家进程的主线程里:HTTP 请求线程里建 root 在 macOS 上会崩,
|
|
5
|
+
Windows 上反复建/销毁也不稳,所以隔离成独立进程。ask_directory() 是父端
|
|
6
|
+
入口,把本文件拉起为子进程,请求参数(JSON:initial 起始目录 / title 标题)
|
|
7
|
+
走 stdin,选中目录以 JSON({"path": ...})走 stdout,用户取消回空串;
|
|
8
|
+
对话框部分是文件尾的 main()。任何一步失败都以非零码退出,父端据此回
|
|
9
|
+
fallback=true 让前端回落网页目录弹框。"""
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
# 非 Windows 置 0:POSIX 的 Popen 对非零 creationflags 抛 ValueError(manager 同款守卫)
|
|
17
|
+
CREATE_NO_WINDOW = 0x08000000 if os.name == "nt" else 0
|
|
18
|
+
_SELF = str(Path(__file__).resolve())
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def ask_directory(initial="", title="选择文件夹"):
|
|
22
|
+
"""父端入口:拉起子进程弹原生对话框,返回 (path, error, fallback)。
|
|
23
|
+
|
|
24
|
+
用户取消时 path 为空串;子进程起不来(机器无 tkinter 等)时 error
|
|
25
|
+
非空且 fallback=True,调用方原样转给前端回落网页目录弹框。"""
|
|
26
|
+
if not Path(__file__).exists():
|
|
27
|
+
return "", "pick_dialog.py 缺失", True
|
|
28
|
+
req = json.dumps({"initial": initial, "title": title}).encode("utf-8")
|
|
29
|
+
env = dict(os.environ, PYTHONIOENCODING="utf-8")
|
|
30
|
+
try:
|
|
31
|
+
if os.name == "nt":
|
|
32
|
+
cp = subprocess.run([sys.executable, _SELF], input=req,
|
|
33
|
+
capture_output=True, timeout=600, env=env,
|
|
34
|
+
creationflags=CREATE_NO_WINDOW)
|
|
35
|
+
else:
|
|
36
|
+
cp = subprocess.run([sys.executable, _SELF], input=req,
|
|
37
|
+
capture_output=True, timeout=600, env=env)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
return "", str(e), True
|
|
40
|
+
if cp.returncode != 0:
|
|
41
|
+
err = (cp.stderr or b"").decode("utf-8", "replace").strip().splitlines()
|
|
42
|
+
return "", (err[-1] if err else "native picker unavailable"), True
|
|
43
|
+
try:
|
|
44
|
+
data = json.loads((cp.stdout or b"").decode("utf-8", "replace") or "{}")
|
|
45
|
+
except Exception:
|
|
46
|
+
return "", "对话框输出无法解析", True
|
|
47
|
+
return str(data.get("path") or ""), "", False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def ask_file(initial="", title="选择文件"):
|
|
51
|
+
"""父端入口:拉起子进程弹原生「选择文件」对话框(导入备份包用)。
|
|
52
|
+
返回 (path, error, fallback),语义与 ask_directory 一致。"""
|
|
53
|
+
if not Path(__file__).exists():
|
|
54
|
+
return "", "pick_dialog.py 缺失", True
|
|
55
|
+
req = json.dumps({"mode": "file", "initial": initial, "title": title}).encode("utf-8")
|
|
56
|
+
env = dict(os.environ, PYTHONIOENCODING="utf-8")
|
|
57
|
+
try:
|
|
58
|
+
if os.name == "nt":
|
|
59
|
+
cp = subprocess.run([sys.executable, _SELF], input=req,
|
|
60
|
+
capture_output=True, timeout=600, env=env,
|
|
61
|
+
creationflags=CREATE_NO_WINDOW)
|
|
62
|
+
else:
|
|
63
|
+
cp = subprocess.run([sys.executable, _SELF], input=req,
|
|
64
|
+
capture_output=True, timeout=600, env=env)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
return "", str(e), True
|
|
67
|
+
if cp.returncode != 0:
|
|
68
|
+
err = (cp.stderr or b"").decode("utf-8", "replace").strip().splitlines()
|
|
69
|
+
return "", (err[-1] if err else "native picker unavailable"), True
|
|
70
|
+
try:
|
|
71
|
+
data = json.loads((cp.stdout or b"").decode("utf-8", "replace") or "{}")
|
|
72
|
+
except Exception:
|
|
73
|
+
return "", "对话框输出无法解析", True
|
|
74
|
+
return str(data.get("path") or ""), "", False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def main():
|
|
78
|
+
req = {}
|
|
79
|
+
try:
|
|
80
|
+
req = json.loads(sys.stdin.read() or "{}")
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
import tkinter as tk
|
|
84
|
+
from tkinter import filedialog
|
|
85
|
+
|
|
86
|
+
root = tk.Tk()
|
|
87
|
+
root.withdraw()
|
|
88
|
+
try:
|
|
89
|
+
root.attributes("-topmost", True) # 别被全屏浏览器盖住
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
is_file = req.get("mode") == "file"
|
|
93
|
+
kw = {"title": req.get("title") or ("选择文件" if is_file else "选择文件夹")}
|
|
94
|
+
init = req.get("initial") or ""
|
|
95
|
+
if init and os.path.isdir(init):
|
|
96
|
+
kw["initialdir"] = init
|
|
97
|
+
try:
|
|
98
|
+
if is_file:
|
|
99
|
+
kw["filetypes"] = [("备份包", "*.zip"), ("全部文件", "*.*")]
|
|
100
|
+
path = filedialog.askopenfilename(**kw) or ""
|
|
101
|
+
else:
|
|
102
|
+
path = filedialog.askdirectory(**kw) or ""
|
|
103
|
+
except Exception:
|
|
104
|
+
sys.exit(1)
|
|
105
|
+
sys.stdout.write(json.dumps({"path": path}))
|
|
106
|
+
root.destroy()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
main()
|