codebee 0.1.34 → 0.1.36
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 +26 -5
- package/README.md +20 -0
- package/app/core/market.py +7 -2
- package/app/core/pipeline.py +3807 -3807
- package/app/core/portscan.py +66 -12
- package/app/core/skills.py +48 -3
- package/app/core/zentao.py +32 -7
- package/app/pet.py +46 -8
- package/app/ui/app.js +96 -33
- package/app/ui/i18n.js +14 -2
- package/app/ui/index.html +26 -22
- package/app/ui/style.css +40 -13
- package/package.json +1 -1
package/app/core/portscan.py
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
- **端口→进程→项目归属推断**:netstat/ss 拿端口→PID,再从进程 CWD 向上走找
|
|
6
6
|
.git/package.json 等项目根——知道该进程属于哪个项目/用户
|
|
7
7
|
- **本地 vs LAN 区分**:127.0.0.1 与 0.0.0.0/LAN 的安全边界
|
|
8
|
-
-
|
|
8
|
+
- **温和关闭、强杀兜底**:先 SIGTERM(Windows taskkill /PID 不带 /F)并
|
|
9
|
+
验证端口真的释放;控制台/服务进程对 WM_CLOSE 无反应,温和无效升级
|
|
10
|
+
强杀,终裁以端口释放为准(关闭前/强杀前都重验 PID)
|
|
9
11
|
|
|
10
12
|
跨平台(Windows netstat + PowerShell / POSIX ss + /proc)纯标准库。
|
|
11
13
|
"""
|
|
@@ -15,7 +17,9 @@ import json
|
|
|
15
17
|
import logging
|
|
16
18
|
import os
|
|
17
19
|
import re
|
|
20
|
+
import signal
|
|
18
21
|
import subprocess
|
|
22
|
+
import time
|
|
19
23
|
|
|
20
24
|
log = logging.getLogger(__name__)
|
|
21
25
|
|
|
@@ -156,8 +160,46 @@ def listening_ports(with_names=True):
|
|
|
156
160
|
return _ports_linux()
|
|
157
161
|
|
|
158
162
|
|
|
163
|
+
def _held_by(port, pid):
|
|
164
|
+
"""端口当前是否仍被指定 PID 监听(轻量扫描,不起 PowerShell)。"""
|
|
165
|
+
return any(pp["port"] == int(port) and pp["pid"] == int(pid)
|
|
166
|
+
for pp in listening_ports(with_names=False))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _wait_release(port, pid, deadline_s):
|
|
170
|
+
"""轮询等待该 PID 释放端口;释放即 True,超时 False。"""
|
|
171
|
+
end = time.time() + deadline_s
|
|
172
|
+
while True:
|
|
173
|
+
if not _held_by(port, pid):
|
|
174
|
+
return True
|
|
175
|
+
if time.time() >= end:
|
|
176
|
+
return False
|
|
177
|
+
time.sleep(0.3)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _win_force_kill(pid):
|
|
181
|
+
"""Windows 强杀:TerminateProcess 直杀根进程(os.kill 的 SIGTERM 语义,
|
|
182
|
+
毫秒级不挂),taskkill /F /T 短等待兜底扫子孙(同 runner._kill_tree 策略;
|
|
183
|
+
taskkill 病态挂死时靠 timeout 放弃,不拖死调用方)。"""
|
|
184
|
+
try:
|
|
185
|
+
os.kill(pid, signal.SIGTERM) # Windows 语义 = TerminateProcess
|
|
186
|
+
except OSError:
|
|
187
|
+
pass # 可能已被温和信号杀掉
|
|
188
|
+
try:
|
|
189
|
+
subprocess.run(["taskkill", "/F", "/T", "/PID", str(pid)],
|
|
190
|
+
timeout=8, capture_output=True)
|
|
191
|
+
except Exception:
|
|
192
|
+
pass # 兜底失败不阻塞终裁验证
|
|
193
|
+
|
|
194
|
+
|
|
159
195
|
def close_port(port):
|
|
160
|
-
"""
|
|
196
|
+
"""关闭端口上的进程。返回 (ok, message)。
|
|
197
|
+
|
|
198
|
+
温和优先(POSIX SIGTERM / Windows WM_CLOSE)并验证端口真的释放。控制台/
|
|
199
|
+
服务进程对 WM_CLOSE 无反应,taskkill 不带 /F 对它们必败——2026-09-21
|
|
200
|
+
用户实测「扫描端口后关不掉」根因,且旧实现不看返回码假报成功。因此:
|
|
201
|
+
温和无效升级强杀,强杀前重验同一 PID 仍监听(防 PID 复用误杀),终裁
|
|
202
|
+
以端口释放为准,绝不假报成功。"""
|
|
161
203
|
for p in listening_ports():
|
|
162
204
|
if p["port"] != port:
|
|
163
205
|
continue
|
|
@@ -169,20 +211,32 @@ def close_port(port):
|
|
|
169
211
|
if p.get("process", "").lower() in _SYSTEM_PROCS:
|
|
170
212
|
return False, "系统服务,不关闭"
|
|
171
213
|
# 重验 PID 仍在监听该端口(防 PID 复用竞态);轻量扫描省掉 PowerShell
|
|
172
|
-
|
|
173
|
-
for pp in listening_ports(with_names=False))
|
|
174
|
-
if not still:
|
|
214
|
+
if not _held_by(port, pid):
|
|
175
215
|
return False, "PID %d 已不在端口 %d 上监听(竞态)" % (pid, port)
|
|
176
216
|
if os.name == "posix":
|
|
177
217
|
try:
|
|
178
218
|
os.kill(pid, 15) # SIGTERM
|
|
179
|
-
return True, "已发送 SIGTERM(PID %d)" % pid
|
|
180
219
|
except OSError as e:
|
|
181
220
|
return False, str(e)[:160]
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
221
|
+
else:
|
|
222
|
+
try:
|
|
223
|
+
subprocess.run(["taskkill", "/PID", str(pid)],
|
|
224
|
+
timeout=10, capture_output=True)
|
|
225
|
+
except Exception:
|
|
226
|
+
pass # 温和失败不在此定论,交给验证+升级
|
|
227
|
+
if _wait_release(port, pid, 3):
|
|
228
|
+
return True, "已关闭(PID %d),端口 %d 已释放" % (pid, port)
|
|
229
|
+
# 温和无效(无窗口进程拒收 WM_CLOSE / 忽略 SIGTERM)→ 升级强杀
|
|
230
|
+
if not _held_by(port, pid):
|
|
231
|
+
return True, "已关闭(PID %d),端口 %d 已释放" % (pid, port)
|
|
232
|
+
if os.name == "posix":
|
|
233
|
+
try:
|
|
234
|
+
os.kill(pid, 9) # SIGKILL
|
|
235
|
+
except OSError as e:
|
|
236
|
+
return False, str(e)[:160]
|
|
237
|
+
else:
|
|
238
|
+
_win_force_kill(pid)
|
|
239
|
+
if _wait_release(port, pid, 3):
|
|
240
|
+
return True, "温和信号无效,已强制结束(PID %d),端口 %d 已释放" % (pid, port)
|
|
241
|
+
return False, "PID %d 未响应关闭,端口 %d 仍被监听,请手动处理" % (pid, port)
|
|
188
242
|
return False, "端口 %d 未找到监听进程" % port
|
package/app/core/skills.py
CHANGED
|
@@ -391,17 +391,25 @@ def relevance_top(lessons, task, limit):
|
|
|
391
391
|
if not probe:
|
|
392
392
|
return lessons[:limit]
|
|
393
393
|
|
|
394
|
-
# 使用反馈闭环(pmb
|
|
394
|
+
# 使用反馈闭环(pmb「量化记忆真实帮助」+ tradememory「按结局加权召回」):
|
|
395
|
+
# 相关性优先;同分比 outcome 胜负(注入后任务过审 +1 / 未过 -1),再比 hits。
|
|
395
396
|
def rank(x):
|
|
396
397
|
grams = _text_bigrams(x.get("title")) | _text_bigrams(x.get("content"))
|
|
397
398
|
overlap = -len(probe & grams)
|
|
399
|
+
karma = int(x.get("won") or 0) - int(x.get("lost") or 0)
|
|
398
400
|
lid = x.get("id") or ""
|
|
399
|
-
return (overlap, -int(x.get("hits") or 0), lid)
|
|
401
|
+
return (overlap, -karma, -int(x.get("hits") or 0), lid)
|
|
400
402
|
|
|
401
403
|
return sorted(lessons, key=rank)[:limit]
|
|
402
404
|
|
|
403
405
|
|
|
404
|
-
|
|
406
|
+
# 运行级注入登记(outcome 加权用):run_id → 注入的教训 id 列表。
|
|
407
|
+
# 容量有界防泄漏;run 收尾 learn_from_run 时消费清除。
|
|
408
|
+
_INJECTED = {}
|
|
409
|
+
_INJECTED_MAX = 200
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def block_for(task, scope_override=None, *, stable_order=False, run_id=None):
|
|
405
413
|
"""生成注入提示词的经验块。命中即计数。返回 (文本, 命中的 id 列表)。
|
|
406
414
|
|
|
407
415
|
3A:内置包 + 用户自建包都参与 scope 匹配;3B:带 persona 的包先注入
|
|
@@ -414,6 +422,9 @@ def block_for(task, scope_override=None, *, stable_order=False):
|
|
|
414
422
|
39 个全文注入会先把 9000 字全局上限吃光,项目教训排在末尾被整段截掉。
|
|
415
423
|
两道预算:①wildcard 包单包限额(定向命中的包不受限);②教训保底——
|
|
416
424
|
包区最多吃到「全局上限 − 教训长度」,教训永远完整注入。
|
|
425
|
+
|
|
426
|
+
run_id(tradememory 借鉴·outcome 加权):登记本次注入的教训,run 收尾
|
|
427
|
+
按结局(过审 +1 / 未过 -1)回写 won/lost——好教训在排序中胜出。
|
|
417
428
|
"""
|
|
418
429
|
scope = scope_override or task.get("type") or "*"
|
|
419
430
|
parts, used, lesson_ids = [], [], []
|
|
@@ -464,9 +475,35 @@ def block_for(task, scope_override=None, *, stable_order=False):
|
|
|
464
475
|
text = text[:MAX_INJECT_CHARS] + "\n…(已截断)"
|
|
465
476
|
if lesson_ids:
|
|
466
477
|
bump_hits(lesson_ids) # 包 id 不参与教训热度,命中数据只保留一份真源
|
|
478
|
+
if run_id:
|
|
479
|
+
with _LOCK:
|
|
480
|
+
if len(_INJECTED) >= _INJECTED_MAX:
|
|
481
|
+
_INJECTED.clear() # 有界兜底:登记超量整体作废(丢信号不丢内存)
|
|
482
|
+
_INJECTED[str(run_id)] = list(lesson_ids)
|
|
467
483
|
return text, used
|
|
468
484
|
|
|
469
485
|
|
|
486
|
+
def note_outcome(run_id, passed):
|
|
487
|
+
"""run 收尾回写注入教训的胜负(tradememory outcome 加权)。
|
|
488
|
+
|
|
489
|
+
passed=True → won+1(这条教训在场时任务过审);False → lost+1。
|
|
490
|
+
只清算登记在案的教训;幂等(同一 run 消费后清除登记)。"""
|
|
491
|
+
rid = str(run_id or "")
|
|
492
|
+
if not rid:
|
|
493
|
+
return
|
|
494
|
+
with _LOCK:
|
|
495
|
+
ids = _INJECTED.pop(rid, None)
|
|
496
|
+
if not ids:
|
|
497
|
+
return
|
|
498
|
+
data = _load()
|
|
499
|
+
idset = set(ids)
|
|
500
|
+
for it in (data.get("lessons") or []):
|
|
501
|
+
if it.get("id") in idset:
|
|
502
|
+
key = "won" if passed else "lost"
|
|
503
|
+
it[key] = int(it.get(key) or 0) + 1
|
|
504
|
+
_save(data)
|
|
505
|
+
|
|
506
|
+
|
|
470
507
|
def bump_hits(ids):
|
|
471
508
|
ids = [i for i in (ids or []) if i]
|
|
472
509
|
if not ids:
|
|
@@ -569,6 +606,14 @@ def learn_from_run(run_id, use_orchestrator=True):
|
|
|
569
606
|
run = store.get_run(run_id)
|
|
570
607
|
if not run:
|
|
571
608
|
return 0
|
|
609
|
+
# outcome 加权(tradememory):本 run 注入过的教训按结局记胜负——
|
|
610
|
+
# 失败 run 说明在场教训没防住这个问题(lost+1),过审则 won+1。
|
|
611
|
+
# 放最前:无论后续是否沉淀新教训,胜负都要落账。
|
|
612
|
+
try:
|
|
613
|
+
verdict = run.get("verdict") or {}
|
|
614
|
+
note_outcome(run_id, bool(verdict.get("pass")))
|
|
615
|
+
except Exception:
|
|
616
|
+
pass
|
|
572
617
|
task = store.get_task(run.get("task_id")) if run.get("task_id") else None
|
|
573
618
|
if not task:
|
|
574
619
|
return 0
|
package/app/core/zentao.py
CHANGED
|
@@ -620,6 +620,33 @@ def _pairs_to_list(pairs, id_key, name_key):
|
|
|
620
620
|
return out
|
|
621
621
|
|
|
622
622
|
|
|
623
|
+
def _module_items(d):
|
|
624
|
+
"""老版模块树形状兼容(2026-09-21 真机反馈「响应形状不认识」):
|
|
625
|
+
sons/modules 数组、{id: {name,...}} 字典、children/sons 嵌套树,统一
|
|
626
|
+
递归展开成 [{id,name}] 平铺清单(id=0 是树根容器,跳过)。"""
|
|
627
|
+
out = []
|
|
628
|
+
|
|
629
|
+
def walk(node):
|
|
630
|
+
if isinstance(node, list):
|
|
631
|
+
for x in node:
|
|
632
|
+
walk(x)
|
|
633
|
+
elif isinstance(node, dict):
|
|
634
|
+
if node.get("id") not in (None, "", 0):
|
|
635
|
+
out.append(node)
|
|
636
|
+
for k in ("children", "sons"):
|
|
637
|
+
if isinstance(node.get(k), (list, dict)):
|
|
638
|
+
walk(node[k])
|
|
639
|
+
else:
|
|
640
|
+
for v in node.values():
|
|
641
|
+
walk(v)
|
|
642
|
+
|
|
643
|
+
if isinstance(d, dict):
|
|
644
|
+
walk(d.get("sons") or d.get("modules") or d.get("tree") or d)
|
|
645
|
+
elif isinstance(d, list):
|
|
646
|
+
walk(d)
|
|
647
|
+
return out
|
|
648
|
+
|
|
649
|
+
|
|
623
650
|
def _old_route(api, method, p, q, body):
|
|
624
651
|
"""路径翻译 + 请求 + 归一。会话死抛 _OldSessionDead。"""
|
|
625
652
|
|
|
@@ -698,16 +725,14 @@ def _old_route(api, method, p, q, body):
|
|
|
698
725
|
m = re.match(r"^/products/(\d+)/modules$", p)
|
|
699
726
|
if m and method == "GET":
|
|
700
727
|
d = go(get("/tree-browse-%s-module.json" % m.group(1)), "(拉模块清单)")
|
|
701
|
-
|
|
702
|
-
if isinstance(d, dict):
|
|
703
|
-
items = d.get("sons") or d.get("modules") or []
|
|
704
|
-
elif isinstance(d, list):
|
|
705
|
-
items = d
|
|
706
|
-
return {"_list": [x for x in items if isinstance(x, dict) and x.get("id")]}
|
|
728
|
+
return {"_list": [x for x in _module_items(d) if isinstance(x, dict) and x.get("id")]}
|
|
707
729
|
|
|
708
730
|
if p == "/products" and method == "GET":
|
|
709
731
|
d = go(get("/api-getmodel-product-getpairs.json"), "(拉产品清单)")
|
|
710
|
-
|
|
732
|
+
prods = d.get("products") if isinstance(d, dict) else None
|
|
733
|
+
if isinstance(prods, list): # 部分老版回数组形状 [{id,name,...}]
|
|
734
|
+
return {"_list": [x for x in prods if isinstance(x, dict) and x.get("id")]}
|
|
735
|
+
pairs = prods if isinstance(prods, dict) else d
|
|
711
736
|
return {"_list": _pairs_to_list(pairs, "id", "name")}
|
|
712
737
|
|
|
713
738
|
if p == "/users" and method == "GET":
|
package/app/pet.py
CHANGED
|
@@ -1090,7 +1090,8 @@ class PetApp:
|
|
|
1090
1090
|
dg = snap.get("digest") or {}
|
|
1091
1091
|
self.seen_digests, notify = digest_alert(self.seen_digests, dg)
|
|
1092
1092
|
if notify:
|
|
1093
|
-
self._show_bubble(digest_bubble_text(dg, self.lang), secs=9.0
|
|
1093
|
+
self._show_bubble(digest_bubble_text(dg, self.lang), secs=9.0,
|
|
1094
|
+
action="digest")
|
|
1094
1095
|
self.last_busy = now
|
|
1095
1096
|
if st == "sleep":
|
|
1096
1097
|
self.mood_kind, self.mood_until = "cheer", now + MOOD_HOLD_S
|
|
@@ -1257,8 +1258,14 @@ class PetApp:
|
|
|
1257
1258
|
self._tip_body = tk.Label(tip, text="", bg="#1B2430",
|
|
1258
1259
|
fg="#ECF2F8",
|
|
1259
1260
|
font=("Microsoft YaHei UI", 9),
|
|
1260
|
-
justify="left", wraplength=280
|
|
1261
|
+
justify="left", wraplength=280,
|
|
1262
|
+
cursor="hand2")
|
|
1261
1263
|
self._tip_body.pack(anchor="w", padx=10, pady=(2, 8))
|
|
1264
|
+
# 点清单即打开网页(含群摘要「点我细看」行——此前清单窗没有任何
|
|
1265
|
+
# 点击绑定,文案写着「点我细看」点了却没反应,用户实测反馈)
|
|
1266
|
+
for w in (self._tip_head, self._tip_body):
|
|
1267
|
+
w.bind("<ButtonPress-1>",
|
|
1268
|
+
lambda e: self._open_ui("#goto=__wxdigest"))
|
|
1262
1269
|
self._tip = tip
|
|
1263
1270
|
self._tip_update()
|
|
1264
1271
|
try:
|
|
@@ -1312,11 +1319,14 @@ class PetApp:
|
|
|
1312
1319
|
text = self._L("alert") % bad.get("title", "?")
|
|
1313
1320
|
self._show_bubble(text)
|
|
1314
1321
|
|
|
1315
|
-
def _show_bubble(self, text, secs=5.0, force=False):
|
|
1322
|
+
def _show_bubble(self, text, secs=5.0, force=False, action=""):
|
|
1323
|
+
"""action:气泡承诺的「点我细看」要兑现(2026-09-21 用户实测点不动——
|
|
1324
|
+
气泡窗口没绑点击,蜜蜂左键也只开首页)。digest=直达群摘要页。"""
|
|
1316
1325
|
if not force and time.time() < self.quiet_until:
|
|
1317
1326
|
return # 「安静一会」期间不碎碎念(系统确认语用 force 放行)
|
|
1318
1327
|
self._destroy_win("_bubble")
|
|
1319
1328
|
self.hop_until = time.time() + 0.7 # 冒泡蹦一下,像在说话
|
|
1329
|
+
self._bubble_action = action
|
|
1320
1330
|
tk = self.tk
|
|
1321
1331
|
bub = tk.Toplevel(self.root)
|
|
1322
1332
|
bub.overrideredirect(True)
|
|
@@ -1324,10 +1334,14 @@ class PetApp:
|
|
|
1324
1334
|
bub.attributes("-topmost", True)
|
|
1325
1335
|
except Exception:
|
|
1326
1336
|
pass
|
|
1327
|
-
tk.Label(bub, text=text, bg="#FFF8E1", fg="#4A3200",
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1337
|
+
lab = tk.Label(bub, text=text, bg="#FFF8E1", fg="#4A3200",
|
|
1338
|
+
bd=1, relief="solid",
|
|
1339
|
+
font=("Microsoft YaHei UI", 9), justify="left",
|
|
1340
|
+
wraplength=230, padx=8, pady=5, cursor="hand2")
|
|
1341
|
+
lab.pack()
|
|
1342
|
+
# 气泡本体可点:点了按承诺跳转(点蜜蜂也走同一逻辑,见 _on_release)
|
|
1343
|
+
for w in (bub, lab):
|
|
1344
|
+
w.bind("<Button-1>", lambda e: self._bubble_click())
|
|
1331
1345
|
x = self.root.winfo_x() + self.win_w - 40
|
|
1332
1346
|
y = max(0, self.root.winfo_y() - 8)
|
|
1333
1347
|
bub.update_idletasks()
|
|
@@ -1338,6 +1352,29 @@ class PetApp:
|
|
|
1338
1352
|
self._bubble_until = time.time() + secs
|
|
1339
1353
|
self.root.after(int(secs * 1000), self._hide_bubble)
|
|
1340
1354
|
|
|
1355
|
+
def _bubble_action_live(self):
|
|
1356
|
+
"""最近一条气泡是否还在展示期且带跳转承诺。"""
|
|
1357
|
+
if time.time() > getattr(self, "_bubble_until", 0):
|
|
1358
|
+
return ""
|
|
1359
|
+
return getattr(self, "_bubble_action", "") or ""
|
|
1360
|
+
|
|
1361
|
+
def _bubble_click(self):
|
|
1362
|
+
# 点了立刻消失并清展示期(_hide_bubble 是到点才收的延迟逻辑,别用);
|
|
1363
|
+
# 先记 action 再清——_open_by_bubble_action 读的就是这个展示期
|
|
1364
|
+
action = self._bubble_action_live()
|
|
1365
|
+
self._destroy_win("_bubble")
|
|
1366
|
+
self._bubble_until = 0
|
|
1367
|
+
if action == "digest":
|
|
1368
|
+
self._open_digest()
|
|
1369
|
+
else:
|
|
1370
|
+
self._open_ui()
|
|
1371
|
+
|
|
1372
|
+
def _open_by_bubble_action(self):
|
|
1373
|
+
if self._bubble_action_live() == "digest":
|
|
1374
|
+
self._open_digest()
|
|
1375
|
+
else:
|
|
1376
|
+
self._open_ui()
|
|
1377
|
+
|
|
1341
1378
|
def _hide_bubble(self):
|
|
1342
1379
|
if time.time() < self._bubble_until:
|
|
1343
1380
|
self.root.after(500, self._hide_bubble)
|
|
@@ -1476,7 +1513,8 @@ class PetApp:
|
|
|
1476
1513
|
self._clamp_pos()
|
|
1477
1514
|
self._save_cfg(x=self.root.winfo_x(), y=self.root.winfo_y())
|
|
1478
1515
|
else:
|
|
1479
|
-
|
|
1516
|
+
# 展示期内气泡承诺过「点我细看」就兑现(digest 直达),否则开首页
|
|
1517
|
+
self._open_by_bubble_action()
|
|
1480
1518
|
|
|
1481
1519
|
def _bye(self, write_setting=False):
|
|
1482
1520
|
"""退出。write_setting=True(右键菜单「关闭桌宠」)先把 pet_enabled=False
|
package/app/ui/app.js
CHANGED
|
@@ -59,7 +59,9 @@ function renderTypeOptions() {
|
|
|
59
59
|
const prev = sel.value; sel.innerHTML = (S.flows || []).map((f) => {
|
|
60
60
|
return '<option value="' + esc(f.id) + '">' + esc(t(f.name)) + t("(") + flowDesc(f) + (f.builtin ? "" : t(" · 自定义")) + t(")") + "</option>";
|
|
61
61
|
}).join("");
|
|
62
|
-
|
|
62
|
+
// 保住用户手动选择(typeTouched);出厂默认 direct 不算选择——prefs 迟到
|
|
63
|
+
// 竞态下要让位给偏好记忆,否则「上次类型」永远恢复不了(高负载下实测)
|
|
64
|
+
if (prev && flowById(prev) && (S.typeTouched || prev !== "direct")) sel.value = prev;
|
|
63
65
|
// 偏好记忆:上次创建用的类型优先于出厂默认「直接执行」(老数据/已删流程回落)
|
|
64
66
|
else if (S.lastPrefs && S.lastPrefs.type && flowById(S.lastPrefs.type)) sel.value = S.lastPrefs.type;
|
|
65
67
|
// 首屏默认落在「直接执行」(快档位:单 CLI 直达,无拆解/评审);
|
|
@@ -153,6 +155,7 @@ function toggleTypeMenu(force) {
|
|
|
153
155
|
}
|
|
154
156
|
|
|
155
157
|
function pickType(id) {
|
|
158
|
+
S.typeTouched = true; // 用户手动选过:flows 重渲染时优先保住,不让位偏好记忆
|
|
156
159
|
const sel = $("f-type");
|
|
157
160
|
if (sel && sel.value !== id) {
|
|
158
161
|
sel.value = id;
|
|
@@ -412,6 +415,16 @@ function operationLabel(path, opts) {
|
|
|
412
415
|
[/\/market\//, "更新插件"],
|
|
413
416
|
[/\/models\/binding$/, "更新智能体绑定"],
|
|
414
417
|
[/\/models\//, "更新模型配置"],
|
|
418
|
+
[/\/zentao\/test$/, "测试禅道连接"],
|
|
419
|
+
[/\/zentao\/products$/, "拉取禅道产品"],
|
|
420
|
+
[/\/zentao\/users$/, "拉取禅道账号"],
|
|
421
|
+
[/\/zentao\/modules$/, "拉取模块清单"],
|
|
422
|
+
[/\/zentao\/scan$/, "禅道扫描 Bug"],
|
|
423
|
+
[/\/zentao\/config$/, "保存禅道配置"],
|
|
424
|
+
[/\/wxdigest\/groups$/, "刷新群列表"],
|
|
425
|
+
[/\/wxdigest\/scan$/, "扫描群摘要"],
|
|
426
|
+
[/\/wxdigest\/config$/, "保存群摘要设置"],
|
|
427
|
+
[/\/ports\/close$/, "关闭端口进程"],
|
|
415
428
|
];
|
|
416
429
|
const hit = rules.find((it) => it[0].test(clean));
|
|
417
430
|
return t(hit ? hit[1] : "提交操作");
|
|
@@ -5733,20 +5746,20 @@ function renderChapterScores(run) {
|
|
|
5733
5746
|
cs.map((c) => {
|
|
5734
5747
|
const means = c.means || {};
|
|
5735
5748
|
const dims = Object.keys(means).map((d) =>
|
|
5736
|
-
'<span class="
|
|
5749
|
+
'<span class="ch-dim' + (means[d] >= 7 ? "" : " low") + '">' + esc(d) + " " + means[d] + "</span>").join("");
|
|
5737
5750
|
const ev = (c.event_check || []).map((line) => {
|
|
5738
5751
|
const ok = /已完成/.test(line), bad = /未完成/.test(line);
|
|
5739
|
-
return '<div class="
|
|
5752
|
+
return '<div class="ch-ev' + (ok ? " ok" : bad ? " bad" : "") + '">' + esc(line) + "</div>";
|
|
5740
5753
|
}).join("");
|
|
5741
|
-
return '<div class="
|
|
5742
|
-
'<div class="
|
|
5743
|
-
'<span class="
|
|
5754
|
+
return '<div class="ch-row' + (c.passed ? "" : " fail") + '">' +
|
|
5755
|
+
'<div class="ch-head"><b>' + esc(t("第") + " " + c.chapter + " " + t("章")) + '</b>' +
|
|
5756
|
+
'<span class="ch-title">' + esc(c.title || "") + "</span>" +
|
|
5744
5757
|
'<span class="tag">' + (c.passed ? t("达标") : t("未达标")) + "</span>" +
|
|
5745
5758
|
(c.reused ? '<span class="tag">' + t("沿用") + "</span>" : "") +
|
|
5746
|
-
'<span class="
|
|
5759
|
+
'<span class="ch-meta">' + Number(c.words || 0) + t(" 字") +
|
|
5747
5760
|
(c.rounds > 1 ? " · " + c.rounds + t(" 轮") : "") + "</span></div>" +
|
|
5748
|
-
(dims ? '<div class="
|
|
5749
|
-
(ev ? '<div class="
|
|
5761
|
+
(dims ? '<div class="ch-dims">' + dims + "</div>" : "") +
|
|
5762
|
+
(ev ? '<div class="ch-evs"><span class="hint">' + t("逐项目标核对:") + "</span>" + ev + "</div>" : "") +
|
|
5750
5763
|
"</div>";
|
|
5751
5764
|
}).join("");
|
|
5752
5765
|
}
|
|
@@ -8904,14 +8917,14 @@ async function testZentao() {
|
|
|
8904
8917
|
}
|
|
8905
8918
|
}
|
|
8906
8919
|
if (r.ok) {
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8920
|
+
// 测试成功即保存当前连接;产品/账号清单后台拉(真机禅道慢,await 会把
|
|
8921
|
+
// 确认按钮按住几十秒——「保存为啥要这么久」根因),拉到后下拉自动就位。
|
|
8922
|
+
api("/api/zentao/config", { method: "POST", operation: false,
|
|
8923
|
+
body: JSON.stringify(ztFormPayload()) })
|
|
8924
|
+
.then(() => loadZentao())
|
|
8925
|
+
.catch(() => {});
|
|
8926
|
+
toast(t("连接成功,已保存;正在拉取产品清单…"));
|
|
8927
|
+
ztFetchCatalog(true).catch(() => {});
|
|
8915
8928
|
}
|
|
8916
8929
|
}
|
|
8917
8930
|
|
|
@@ -8959,19 +8972,39 @@ function mkCharsText(n) {
|
|
|
8959
8972
|
return t("约 ") + n + " " + t("字");
|
|
8960
8973
|
}
|
|
8961
8974
|
|
|
8962
|
-
function mkCardHtml(p) {
|
|
8975
|
+
function mkCardHtml(p, section) {
|
|
8963
8976
|
let ops;
|
|
8964
8977
|
if (p.builtin) ops = '<button class="ghost small" disabled>' + t("已内置") + "</button>";
|
|
8965
8978
|
else if (p.installed) ops = '<span class="tag ok">' + t("已安装") + "</span>" +
|
|
8966
8979
|
'<button class="ghost small" onclick="mkRemove(\'' + esc(p.id) + '\')">' + t("卸载") + "</button>";
|
|
8967
8980
|
else ops = '<button class="primary small" onclick="mkInstall(\'' + esc(p.id) + '\')">' + t("安装") + "</button>";
|
|
8981
|
+
// 已装插件卡片直接给启停开关(对齐 ZCode 插件管理形态:启停不用去经验库翻)
|
|
8982
|
+
const toggle = p.installed
|
|
8983
|
+
? '<label class="switch" title="' + (p.enabled ? t("已启用") : t("已停用")) + '">' +
|
|
8984
|
+
'<input type="checkbox"' + (p.enabled ? " checked" : "") +
|
|
8985
|
+
' onchange="mkTogglePack(\'' + esc(p.id) + '\', this.checked)"></label>'
|
|
8986
|
+
: "";
|
|
8987
|
+
const sec = section ? '<span class="tag mk-sec">' + esc(section) + "</span>" : "";
|
|
8968
8988
|
return '<div class="card mk-card"><div class="head">' +
|
|
8969
8989
|
'<span class="name">' + esc(t(p.name)) + "</span>" +
|
|
8970
|
-
'<span class="tag">' + esc(t(p.category || "")) + "</span>" +
|
|
8990
|
+
'<span class="tag">' + esc(t(p.category || "")) + "</span>" + sec +
|
|
8971
8991
|
"</div>" +
|
|
8972
8992
|
'<div class="note">' + esc(t(p.desc || "")) + "</div>" +
|
|
8973
8993
|
'<div class="mk-meta"><span title="' + esc(t("运行该类型任务时自动注入提示词,无需手动调用")) + '">' + t("适用:") + esc(mkScopeText(p.scopes)) + "</span><span>" + esc(mkCharsText(p.chars)) + "</span></div>" +
|
|
8974
|
-
'<div class="ops">' + ops + "</div></div>";
|
|
8994
|
+
'<div class="ops">' + ops + toggle + "</div></div>";
|
|
8995
|
+
}
|
|
8996
|
+
|
|
8997
|
+
/* 已装插件启停:走经验库包启停(同一条链),停用后该插件不再注入任务提示词 */
|
|
8998
|
+
async function mkTogglePack(id, enabled) {
|
|
8999
|
+
try {
|
|
9000
|
+
await api("/api/skills/pack-op", { method: "POST",
|
|
9001
|
+
body: JSON.stringify({ id, op: enabled ? "enable" : "disable" }) });
|
|
9002
|
+
toast(enabled ? t("已启用") : t("已停用(不再注入任务提示词)"));
|
|
9003
|
+
loadMarket();
|
|
9004
|
+
} catch (e) {
|
|
9005
|
+
toast(e.message || t("操作失败"), true);
|
|
9006
|
+
loadMarket();
|
|
9007
|
+
}
|
|
8975
9008
|
}
|
|
8976
9009
|
|
|
8977
9010
|
function renderMarket() {
|
|
@@ -9001,7 +9034,22 @@ function renderMarket() {
|
|
|
9001
9034
|
(p.name || "").toLowerCase().indexOf(q) >= 0 || (p.desc || "").toLowerCase().indexOf(q) >= 0);
|
|
9002
9035
|
const cnt = $("mk-count");
|
|
9003
9036
|
if (cnt && mkActiveView() === "local") cnt.textContent = t("共 ") + items.length + t(" 个");
|
|
9004
|
-
|
|
9037
|
+
// 分区渲染(对齐 ZCode 插件管理形态):内置 / 已安装 / 可安装三组,
|
|
9038
|
+
// 各组小标题+卡片;「全部」以外状态只出命中的那组
|
|
9039
|
+
if (st === "all" && !cat && !q) {
|
|
9040
|
+
const builtin = items.filter((p) => p.builtin);
|
|
9041
|
+
const inst = items.filter((p) => !p.builtin && p.installed);
|
|
9042
|
+
const avail = items.filter((p) => !p.builtin && !p.installed);
|
|
9043
|
+
const sec = (title, list) => list.length
|
|
9044
|
+
? '<h3 class="sec-title">' + title + " " + list.length + "</h3>" +
|
|
9045
|
+
'<div class="mk-grid">' + list.map((p) => mkCardHtml(p, t(title))).join("") + "</div>"
|
|
9046
|
+
: "";
|
|
9047
|
+
grid.innerHTML =
|
|
9048
|
+
sec(t("内置"), builtin) + sec(t("已安装"), inst) + sec(t("可安装"), avail) ||
|
|
9049
|
+
'<div class="empty">' + t("没有符合条件插件") + "</div>";
|
|
9050
|
+
return;
|
|
9051
|
+
}
|
|
9052
|
+
grid.innerHTML = items.map((p) => mkCardHtml(p)).join("") || '<div class="empty">' + t("没有符合条件插件") + "</div>";
|
|
9005
9053
|
}
|
|
9006
9054
|
|
|
9007
9055
|
async function mkInstall(id) {
|
|
@@ -9879,10 +9927,10 @@ async function scanPorts() {
|
|
|
9879
9927
|
}
|
|
9880
9928
|
|
|
9881
9929
|
async function closePort(port) {
|
|
9882
|
-
if (!confirm(t("
|
|
9930
|
+
if (!confirm(t("结束占用端口 %1 的进程?先温和关闭,无效会自动强制结束(系统进程会被拒绝)").replace("%1", port))) return;
|
|
9883
9931
|
try {
|
|
9884
9932
|
const r = await api("/api/ports/close", { method: "POST", body: JSON.stringify({ port }) });
|
|
9885
|
-
toast(r.message || (r.ok ? t("
|
|
9933
|
+
toast(r.message || (r.ok ? t("已关闭") : t("未能关闭")), !r.ok);
|
|
9886
9934
|
scanPorts();
|
|
9887
9935
|
} catch (e) {
|
|
9888
9936
|
toast(e.message || t("关闭失败"), true);
|
|
@@ -11649,16 +11697,24 @@ async function beeRefreshGroups() {
|
|
|
11649
11697
|
const old = btn.textContent;
|
|
11650
11698
|
btn.textContent = t("刷新中…");
|
|
11651
11699
|
try {
|
|
11700
|
+
// 后端 refresh_groups 读的是已保存配置的 reader_python;用户选完候选
|
|
11701
|
+
// 直接点刷新(不点保存)时服务端还是旧值——先静默落盘再刷新
|
|
11702
|
+
// (「填了路径仍报请先填」根因)
|
|
11703
|
+
if (!(await beeSaveCore())) { return; }
|
|
11652
11704
|
const r = await api("/api/wxdigest/groups", { method: "POST", body: "{}", timeout: 180000 });
|
|
11653
11705
|
if (Bee.view) Bee.view.reader_groups_cache = r.groups || [];
|
|
11654
11706
|
toast(t("群列表已更新:") + (r.groups || []).length);
|
|
11655
11707
|
} catch (e) { toast(t("刷新失败:") + (e.message || e), true); }
|
|
11656
|
-
|
|
11657
|
-
|
|
11658
|
-
|
|
11708
|
+
finally {
|
|
11709
|
+
btn.disabled = false;
|
|
11710
|
+
btn.textContent = old;
|
|
11711
|
+
beeRenderGroups();
|
|
11712
|
+
}
|
|
11659
11713
|
}
|
|
11660
11714
|
|
|
11661
|
-
async function
|
|
11715
|
+
async function beeSaveCore() {
|
|
11716
|
+
/* 把表单当前值落到服务端;成功返回 true,失败已 toast。不碰勾选态
|
|
11717
|
+
(beeSave 的 selGroups 置空依赖随后的 beeRefresh 重建,这里没有)。 */
|
|
11662
11718
|
try {
|
|
11663
11719
|
const r = await api("/api/wxdigest/config", { method: "POST", body: JSON.stringify({
|
|
11664
11720
|
enabled: $("bee-enabled").checked,
|
|
@@ -11670,11 +11726,16 @@ async function beeSave() {
|
|
|
11670
11726
|
groups: Bee.selGroups || [],
|
|
11671
11727
|
}) });
|
|
11672
11728
|
if (Bee.view) Bee.view.config = r.config;
|
|
11673
|
-
Bee.
|
|
11674
|
-
|
|
11675
|
-
|
|
11676
|
-
|
|
11677
|
-
|
|
11729
|
+
Bee.pyDirty = false; // 保存成功:服务端值与所见一致,恢复轮询回填
|
|
11730
|
+
return true;
|
|
11731
|
+
} catch (e) { toast(t("保存失败:") + (e.message || e), true); return false; }
|
|
11732
|
+
}
|
|
11733
|
+
|
|
11734
|
+
async function beeSave() {
|
|
11735
|
+
if (!(await beeSaveCore())) return;
|
|
11736
|
+
Bee.selGroups = null; // 下次渲染用保存后的值重建勾选态
|
|
11737
|
+
toast(t("群摘要设置已保存"));
|
|
11738
|
+
beeRefresh();
|
|
11678
11739
|
}
|
|
11679
11740
|
|
|
11680
11741
|
async function beeScan() {
|
|
@@ -11682,13 +11743,15 @@ async function beeScan() {
|
|
|
11682
11743
|
if (!btn || btn.disabled) return;
|
|
11683
11744
|
btn.disabled = true;
|
|
11684
11745
|
try {
|
|
11746
|
+
// 微信直连拉取依赖 reader_python,先静默落盘(同 beeRefreshGroups)
|
|
11747
|
+
if (!(await beeSaveCore())) { return; }
|
|
11685
11748
|
const r = await api("/api/wxdigest/scan", { method: "POST", body: "{}", timeout: 300000 });
|
|
11686
11749
|
if (r.ok && r.made) toast(t("扫描完成,生成") + " " + r.made + t(" 条新摘要"));
|
|
11687
11750
|
else if (r.ok) toast(t("扫描完成,暂无新内容"));
|
|
11688
11751
|
else toast(t("扫描有问题:") + (r.error || ""), true);
|
|
11689
11752
|
beeRefresh();
|
|
11690
11753
|
} catch (e) { toast(t("扫描失败:") + (e.message || e), true); }
|
|
11691
|
-
btn.disabled = false;
|
|
11754
|
+
finally { btn.disabled = false; }
|
|
11692
11755
|
}
|
|
11693
11756
|
|
|
11694
11757
|
(function () {
|
package/app/ui/i18n.js
CHANGED
|
@@ -1281,6 +1281,8 @@
|
|
|
1281
1281
|
"约 ": "about ",
|
|
1282
1282
|
"字": "chars",
|
|
1283
1283
|
"已内置": "Built-in",
|
|
1284
|
+
"已启用": "Enabled",
|
|
1285
|
+
"已停用(不再注入任务提示词)": "Disabled — no longer injected into task prompts",
|
|
1284
1286
|
"没有符合条件插件": "No plugins match the filters",
|
|
1285
1287
|
"该插件已安装过": "Already installed",
|
|
1286
1288
|
"安装成功,运行任务时自动注入提示词;可到「经验库」启停或查看": "Installed — auto-injected into prompts when tasks run; enable/disable or view it in the skill library",
|
|
@@ -1327,6 +1329,16 @@
|
|
|
1327
1329
|
"更新插件": "Update plugin",
|
|
1328
1330
|
"更新模型配置": "Update model settings",
|
|
1329
1331
|
"更新智能体绑定": "Update agent binding",
|
|
1332
|
+
"测试禅道连接": "Test ZenTao connection",
|
|
1333
|
+
"拉取禅道产品": "Fetch ZenTao products",
|
|
1334
|
+
"拉取禅道账号": "Fetch ZenTao accounts",
|
|
1335
|
+
"拉取模块清单": "Fetch module list",
|
|
1336
|
+
"禅道扫描 Bug": "Scan ZenTao bugs",
|
|
1337
|
+
"保存禅道配置": "Save ZenTao settings",
|
|
1338
|
+
"刷新群列表": "Refresh group list",
|
|
1339
|
+
"扫描群摘要": "Scan digests",
|
|
1340
|
+
"保存群摘要设置": "Save digest settings",
|
|
1341
|
+
"关闭端口进程": "Close port process",
|
|
1330
1342
|
"提交操作": "Submit operation",
|
|
1331
1343
|
"{0} 项处理中": "{0} in progress",
|
|
1332
1344
|
"{0} 项失败": "{0} failed",
|
|
@@ -1950,8 +1962,8 @@
|
|
|
1950
1962
|
"关闭": "Close",
|
|
1951
1963
|
"没有发现监听端口": "No listening ports found",
|
|
1952
1964
|
"扫描失败:": "Scan failed: ",
|
|
1953
|
-
"
|
|
1954
|
-
"
|
|
1965
|
+
"结束占用端口 %1 的进程?先温和关闭,无效会自动强制结束(系统进程会被拒绝)": "Kill the process holding port %1? Gentle close first, forced kill if ignored (system processes are refused)",
|
|
1966
|
+
"已关闭": "Closed",
|
|
1955
1967
|
"未能关闭": "Could not close",
|
|
1956
1968
|
"关闭失败": "Close failed",
|
|
1957
1969
|
|