codebee 0.1.26 → 0.1.28
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 +31 -13
- package/README.md +20 -4
- package/app/core/manager.py +41 -32
- package/app/core/modelhub.py +1 -0
- package/app/core/prefs.py +74 -70
- package/app/core/runner.py +41 -20
- package/app/ui/app.js +32 -4
- package/app/ui/i18n.js +6 -0
- package/app/ui/index.html +4 -4
- package/app/ui/style.css +13 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,19 +4,37 @@ CodeBee 的用户可感知变更记录。发布新版时:最上面加一节,
|
|
|
4
4
|
`<!-- relnotes:start -->…<!-- relnotes:end -->` 段(那段会被 `npm view` 的
|
|
5
5
|
README 元数据带回,供老版本在「发现新版本」时展示新版更新内容)。
|
|
6
6
|
|
|
7
|
-
## 未发布
|
|
8
|
-
|
|
9
|
-
## v0.1.
|
|
10
|
-
|
|
11
|
-
###
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
7
|
+
## 未发布
|
|
8
|
+
|
|
9
|
+
## v0.1.28(2026-09-21)
|
|
10
|
+
|
|
11
|
+
### 🛠 问题修复
|
|
12
|
+
|
|
13
|
+
- 「关于与更新」页的版本信息与更新说明改为左对齐阅读,不再整块居中(加载占位的居中样式误留在了正式内容上)。
|
|
14
|
+
|
|
15
|
+
## v0.1.27(2026-09-21)
|
|
16
|
+
|
|
17
|
+
### ✨ 新功能
|
|
18
|
+
|
|
19
|
+
- 连载任务防穿帮:新增「资源账本」——每章评审自动登记新出现的道具、伤情、承诺与伏笔,写下一章前自动注入提醒核对,角色不会上一章断臂下一章就挥剑。
|
|
20
|
+
- 连载评审逐项核对:每章按大纲要点逐项判定「已完成/未完成/待核实」并引用正文证据,铺垫不再被当成已完成。
|
|
21
|
+
- 新建任务记住你的偏好:上次选择的任务类型、轮数、阈值、章节数等下次自动预填(只记参数,不记内容)。
|
|
22
|
+
|
|
23
|
+
### 🛠 问题修复
|
|
24
|
+
|
|
25
|
+
- 代码任务提速:验证命令失败时先直接修复再评审(首轮不再白跑一次评审),连续失败两轮后才请评审参与诊断。
|
|
26
|
+
|
|
27
|
+
## v0.1.26(2026-09-21)
|
|
28
|
+
|
|
29
|
+
### ✨ 新功能
|
|
30
|
+
|
|
31
|
+
- 顶栏新增统一操作状态中心:创建、删除、绑定、插件等用户写操作会显示进行中、完成或失败状态,并保留时间与错误原因,慢请求不再像页面卡死。
|
|
32
|
+
|
|
33
|
+
### 🛠 问题修复
|
|
34
|
+
|
|
35
|
+
- 后台维护写请求、任务澄清探测和轮询请求默认静默,避免把后台动作误显示成用户操作;移动端状态弹层自适应屏幕并支持减少动画。
|
|
36
|
+
|
|
37
|
+
## v0.1.25(2026-09-21)
|
|
20
38
|
|
|
21
39
|
### ✨ 新功能
|
|
22
40
|
|
package/README.md
CHANGED
|
@@ -21,15 +21,31 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
|
|
|
21
21
|
不会把你的任务内容交给任何第三方。
|
|
22
22
|
|
|
23
23
|
<!-- relnotes:start -->
|
|
24
|
+
### 🛠 问题修复 · Fixes
|
|
25
|
+
|
|
26
|
+
- 「关于与更新」页的版本信息与更新说明改为左对齐阅读,不再整块居中。
|
|
27
|
+
- The version info and release notes on the About page are now left-aligned for easier reading.
|
|
28
|
+
|
|
24
29
|
### ✨ 新功能 · New
|
|
25
30
|
|
|
26
|
-
-
|
|
27
|
-
-
|
|
31
|
+
- 连载任务防穿帮:新增「资源账本」——每章评审自动登记新出现的道具、伤情、承诺与伏笔,写下一章前自动注入提醒核对。
|
|
32
|
+
- Serial tasks now keep a resource ledger: props, injuries, promises, and plot threads are tracked per chapter and injected before writing the next one.
|
|
33
|
+
|
|
34
|
+
- 连载评审逐项核对:每章按大纲要点逐项判定「已完成/未完成/待核实」并引用正文证据,铺垫不再被当成已完成。
|
|
35
|
+
- Chapter reviews now check every outline beat against the actual text — foreshadowing no longer counts as done.
|
|
36
|
+
|
|
37
|
+
- 新建任务记住你的偏好:上次选择的任务类型、轮数、阈值、章节数等下次自动预填(只记参数,不记内容)。
|
|
38
|
+
- The new-task form now remembers your last-used type, rounds, threshold, and chapter settings (parameters only, never content).
|
|
28
39
|
|
|
29
40
|
### 🛠 问题修复 · Fixes
|
|
30
41
|
|
|
31
|
-
-
|
|
32
|
-
-
|
|
42
|
+
- 代码任务提速:验证命令失败时先直接修复再评审(首轮不再白跑一次评审),连续失败两轮后才请评审参与诊断。
|
|
43
|
+
- Code tasks get faster: when verification fails, the agent fixes first and review joins only after repeated failures.
|
|
44
|
+
|
|
45
|
+
### 上一版亮点 · Previous release
|
|
46
|
+
|
|
47
|
+
- 顶栏新增统一操作状态中心:创建、删除、绑定、插件等写操作会显示进行中、完成或失败状态,并保留时间与错误原因。
|
|
48
|
+
- A unified operation center now shows create, delete, binding, and plugin actions as running, completed, or failed, with timestamps and error details.
|
|
33
49
|
|
|
34
50
|
### 上一版亮点 · Previous release
|
|
35
51
|
|
package/app/core/manager.py
CHANGED
|
@@ -31,18 +31,18 @@ _LOCK = threading.RLock()
|
|
|
31
31
|
_STATE = {"detected": {}, "versions": {}, "detect_ts": 0.0, "detect_ev": None}
|
|
32
32
|
|
|
33
33
|
# 能自动写入默认模型的 config.format(其余格式只能手动编辑)
|
|
34
|
-
_WRITABLE_FORMATS = ("toml-line", "toml-section", "json", "json-path", "jsonc",
|
|
35
|
-
"yaml-line")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _read_text(path, preserve_newlines=False):
|
|
39
|
-
"""读取文本并及时关闭句柄;改写配置前可保留原始换行符。"""
|
|
40
|
-
newline = "" if preserve_newlines else None
|
|
41
|
-
with open(path, encoding="utf-8", errors="replace", newline=newline) as fh:
|
|
42
|
-
return fh.read()
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _expand(p):
|
|
34
|
+
_WRITABLE_FORMATS = ("toml-line", "toml-section", "json", "json-path", "jsonc",
|
|
35
|
+
"yaml-line")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _read_text(path, preserve_newlines=False):
|
|
39
|
+
"""读取文本并及时关闭句柄;改写配置前可保留原始换行符。"""
|
|
40
|
+
newline = "" if preserve_newlines else None
|
|
41
|
+
with open(path, encoding="utf-8", errors="replace", newline=newline) as fh:
|
|
42
|
+
return fh.read()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _expand(p):
|
|
46
46
|
return os.path.abspath(os.path.expanduser(os.path.expandvars(p)))
|
|
47
47
|
|
|
48
48
|
|
|
@@ -482,7 +482,7 @@ def read_model(entry):
|
|
|
482
482
|
if not path or not os.path.isfile(path) or not cfg.get("format"):
|
|
483
483
|
return None
|
|
484
484
|
try:
|
|
485
|
-
text = _read_text(path)
|
|
485
|
+
text = _read_text(path)
|
|
486
486
|
except Exception:
|
|
487
487
|
return None
|
|
488
488
|
if cfg["format"] == "toml-line":
|
|
@@ -554,7 +554,7 @@ def write_model(entry, model):
|
|
|
554
554
|
# mimo(mimocode.jsonc)有注释,整体重解析会丢注释——复用
|
|
555
555
|
# _jsonc_set 做就地片段改写(只动目标键,其余原样保留)
|
|
556
556
|
keys = (cfg.get("model_key") or "model").split(".")
|
|
557
|
-
text = _read_text(path, preserve_newlines=True)
|
|
557
|
+
text = _read_text(path, preserve_newlines=True)
|
|
558
558
|
new_text, ok = _jsonc_set(text, tuple(keys),
|
|
559
559
|
json.dumps(model, ensure_ascii=False))
|
|
560
560
|
if not ok:
|
|
@@ -562,7 +562,7 @@ def write_model(entry, model):
|
|
|
562
562
|
"error": "jsonc 结构异常,未能就地写入 %s(已避免覆盖)" % path}
|
|
563
563
|
Path(path).write_bytes(new_text.encode("utf-8"))
|
|
564
564
|
elif fmt == "toml-line":
|
|
565
|
-
text = _read_text(path)
|
|
565
|
+
text = _read_text(path)
|
|
566
566
|
new_line = 'model = "%s"' % model
|
|
567
567
|
if re.search(r'(?m)^\s*model\s*=\s*"[^"]*"', text):
|
|
568
568
|
text = re.sub(r'(?m)^\s*model\s*=\s*"[^"]*"', new_line, text)
|
|
@@ -570,19 +570,19 @@ def write_model(entry, model):
|
|
|
570
570
|
text = text.rstrip("\n") + "\n" + new_line + "\n"
|
|
571
571
|
Path(path).write_bytes(text.encode("utf-8"))
|
|
572
572
|
elif fmt == "toml-section":
|
|
573
|
-
text = _read_text(path, preserve_newlines=True)
|
|
573
|
+
text = _read_text(path, preserve_newlines=True)
|
|
574
574
|
text = _toml_write_value(text, *_dotted_key(cfg), value=model)
|
|
575
575
|
Path(path).write_bytes(text.encode("utf-8"))
|
|
576
576
|
elif fmt == "yaml-line":
|
|
577
577
|
# newline="" 关掉通用换行转换:文本层面看不出 \r\n 就会被静默改写成 LF,
|
|
578
578
|
# 用户的 Windows 配置不该因为写个模型名而整篇换行符被替换
|
|
579
|
-
text = _read_text(path, preserve_newlines=True)
|
|
579
|
+
text = _read_text(path, preserve_newlines=True)
|
|
580
580
|
text = _yaml_write_value(text, *_yaml_model_path(cfg), value=model)
|
|
581
581
|
Path(path).write_bytes(text.encode("utf-8"))
|
|
582
582
|
elif fmt == "json-path":
|
|
583
583
|
# openclaw(agents.defaults.model.primary):默认模型藏在嵌套对象里,
|
|
584
584
|
# 且 openclaw 对未知顶层键直接拒绝启动——绝不能写顶层 "model"
|
|
585
|
-
text = _read_text(path, preserve_newlines=True)
|
|
585
|
+
text = _read_text(path, preserve_newlines=True)
|
|
586
586
|
try:
|
|
587
587
|
data = json.loads(text) if text.strip() else {}
|
|
588
588
|
except Exception:
|
|
@@ -604,7 +604,7 @@ def write_model(entry, model):
|
|
|
604
604
|
json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8"))
|
|
605
605
|
else:
|
|
606
606
|
try:
|
|
607
|
-
data = json.loads(_read_text(path))
|
|
607
|
+
data = json.loads(_read_text(path))
|
|
608
608
|
except FileNotFoundError:
|
|
609
609
|
return {"ok": False,
|
|
610
610
|
"error": "配置文件尚未生成(%s 首次运行后才有),暂无法写入" % path}
|
|
@@ -662,7 +662,7 @@ def _launch_log_path(entry):
|
|
|
662
662
|
def _best_url(log_path, port):
|
|
663
663
|
"""从启动日志提取该端口的信任 URL(含 token 优先)。无日志/未匹配返回 None。"""
|
|
664
664
|
try:
|
|
665
|
-
text = _read_text(str(log_path))
|
|
665
|
+
text = _read_text(str(log_path))
|
|
666
666
|
except Exception:
|
|
667
667
|
return None
|
|
668
668
|
urls = re.findall(r"https?://[^\s\"'<>]+", text)
|
|
@@ -762,7 +762,7 @@ def _sync_dsh_settings(entry, model, base_url):
|
|
|
762
762
|
return "dsh 配置路径越出用户主目录,已拒绝"
|
|
763
763
|
try:
|
|
764
764
|
if os.path.isfile(path):
|
|
765
|
-
text = _read_text(path, preserve_newlines=True)
|
|
765
|
+
text = _read_text(path, preserve_newlines=True)
|
|
766
766
|
else:
|
|
767
767
|
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
768
768
|
text = ""
|
|
@@ -785,7 +785,7 @@ def _dsh_selfcheck_model(entry):
|
|
|
785
785
|
if not path or not os.path.isfile(path):
|
|
786
786
|
return None, ""
|
|
787
787
|
try:
|
|
788
|
-
text = _read_text(path, preserve_newlines=True)
|
|
788
|
+
text = _read_text(path, preserve_newlines=True)
|
|
789
789
|
except Exception:
|
|
790
790
|
return None, ""
|
|
791
791
|
cur = _yaml_read_value(text, "agent-default-model", "model")
|
|
@@ -809,7 +809,7 @@ def _dsh_key_present():
|
|
|
809
809
|
return True
|
|
810
810
|
try:
|
|
811
811
|
envfile = os.path.join(os.path.expanduser("~"), ".dsh", ".env")
|
|
812
|
-
return "DEEPSEEK_API_KEY" in _read_text(envfile)
|
|
812
|
+
return "DEEPSEEK_API_KEY" in _read_text(envfile)
|
|
813
813
|
except Exception:
|
|
814
814
|
return False
|
|
815
815
|
|
|
@@ -915,7 +915,7 @@ def _sync_codex_settings(entry, model, cp):
|
|
|
915
915
|
("wire_api", q(cp.get("wire_api", "responses")))]
|
|
916
916
|
try:
|
|
917
917
|
if os.path.isfile(path):
|
|
918
|
-
text = _read_text(path, preserve_newlines=True)
|
|
918
|
+
text = _read_text(path, preserve_newlines=True)
|
|
919
919
|
else:
|
|
920
920
|
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
921
921
|
text = ""
|
|
@@ -1122,7 +1122,7 @@ def _sync_settings_env(path, updates, remove_keys=()):
|
|
|
1122
1122
|
不覆盖;改动前 .bak。返回错误串或 None。path 必须已过主目录围栏校验。"""
|
|
1123
1123
|
try:
|
|
1124
1124
|
if os.path.isfile(path):
|
|
1125
|
-
text = _read_text(path)
|
|
1125
|
+
text = _read_text(path)
|
|
1126
1126
|
try:
|
|
1127
1127
|
data = json.loads(text)
|
|
1128
1128
|
except Exception:
|
|
@@ -1242,7 +1242,7 @@ def _sync_opencode_settings(entry, model, prov):
|
|
|
1242
1242
|
errs.append("路径越出主目录:%s" % path)
|
|
1243
1243
|
continue
|
|
1244
1244
|
if os.path.isfile(path):
|
|
1245
|
-
text = _read_text(path, preserve_newlines=True)
|
|
1245
|
+
text = _read_text(path, preserve_newlines=True)
|
|
1246
1246
|
else:
|
|
1247
1247
|
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
1248
1248
|
text = ""
|
|
@@ -1333,7 +1333,7 @@ def _sync_kimi_settings(entry, model, prov):
|
|
|
1333
1333
|
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
1334
1334
|
text = ""
|
|
1335
1335
|
if os.path.isfile(path):
|
|
1336
|
-
text = _read_text(path, preserve_newlines=True)
|
|
1336
|
+
text = _read_text(path, preserve_newlines=True)
|
|
1337
1337
|
shutil.copyfile(path, path + ".bak")
|
|
1338
1338
|
# 移除旧托管块与旧顶层键(防重复/防键落进别的表)
|
|
1339
1339
|
text = _re.sub(r"# >>> CodeBee managed.*?# <<< CodeBee managed <<<\n?",
|
|
@@ -1410,7 +1410,7 @@ def _sync_agent_injection(entry, binding):
|
|
|
1410
1410
|
if entry["id"] in _NO_CHANNEL_HINT:
|
|
1411
1411
|
if binding.get("env"):
|
|
1412
1412
|
return "已按绑定注入 env,但该 CLI 未必认 CodeBee 的凭据通道,打开后若要求登录请在其界面内登录"
|
|
1413
|
-
return "未指定可用供应商:打开后使用 CLI 自带登录;需要固定注入请到「模型调度(可选)」页指定"
|
|
1413
|
+
return "未指定可用供应商:打开后使用 CLI 自带登录;需要固定注入请到「模型调度(可选)」页指定"
|
|
1414
1414
|
return None
|
|
1415
1415
|
|
|
1416
1416
|
|
|
@@ -1418,8 +1418,9 @@ def _sync_launch_model(entry, binding):
|
|
|
1418
1418
|
"""打开前把「CLI 绑定」页选中的模型落到该 CLI 自己的配置文件——保证
|
|
1419
1419
|
交互/网页版启动即选中绑定模型(绑定页是运行时模型唯一真源,目录页的
|
|
1420
1420
|
「默认模型」只是手动快照,会滞后)。dsh 额外同步端点与 models 目录;
|
|
1421
|
-
codex
|
|
1422
|
-
|
|
1421
|
+
codex 已改为不落盘(2026-09-21 起:全局 model_provider 归 CC Switch/
|
|
1422
|
+
用户管理,编排任务的绑定走 runner 的 -c 一次性注入)。返回给用户
|
|
1423
|
+
看的同步笔记列表。"""
|
|
1423
1424
|
notes = []
|
|
1424
1425
|
model = (binding.get("model") or "").strip()
|
|
1425
1426
|
prov = binding.get("provider") or {}
|
|
@@ -1427,6 +1428,14 @@ def _sync_launch_model(entry, binding):
|
|
|
1427
1428
|
is_dsh = entry["id"] in ("deepseek-harness", "dsh")
|
|
1428
1429
|
is_codex = entry["id"] in ("codex-cli", "codex")
|
|
1429
1430
|
cp = binding.get("codex_provider")
|
|
1431
|
+
if is_codex:
|
|
1432
|
+
# codex 全局配置不落盘(2026-09-21 用户决策):全局 model_provider 交还
|
|
1433
|
+
# CC Switch/用户手动管理,一键打开不再顶掉外部选择。编排任务不受影响
|
|
1434
|
+
# ——runner 的 -c 一次性注入仍带绑定供应商;交互 TUI 打开即用外部当前
|
|
1435
|
+
# 配置,想用绑定供应商起编排任务即可。
|
|
1436
|
+
notes.append("codex 配置保持外部管理(绑定 %s 仅用于编排 -c 注入,未写 config.toml)"
|
|
1437
|
+
% (cp.get("name") or "orch"))
|
|
1438
|
+
return notes
|
|
1430
1439
|
if model and fmt in _WRITABLE_FORMATS:
|
|
1431
1440
|
w = write_model(entry, model)
|
|
1432
1441
|
notes.append("模型已同步为 %s" % w["model"] if w.get("ok")
|
|
@@ -1530,7 +1539,7 @@ def launch(entry, open_browser=True):
|
|
|
1530
1539
|
notes = _sync_launch_model(entry, binding)
|
|
1531
1540
|
if is_dsh and not (binding.get("env") or {}) and not _dsh_key_present():
|
|
1532
1541
|
notes.append("未发现 dsh 密钥:打开后可能需在 dsh 内登录配置;"
|
|
1533
|
-
"要打开即用,请到「模型调度(可选)」页给 DeepSeek Harness 指定 openai 协议供应商")
|
|
1542
|
+
"要打开即用,请到「模型调度(可选)」页给 DeepSeek Harness 指定 openai 协议供应商")
|
|
1534
1543
|
name = entry.get("name", entry["id"])
|
|
1535
1544
|
kind = (launch.get("kind") or "console").lower()
|
|
1536
1545
|
extra = (";".join(notes)) if notes else ""
|
|
@@ -1637,7 +1646,7 @@ def _open_when_ready(port, url, log_path, timeout=30):
|
|
|
1637
1646
|
# ---------------------------------------------------------------- 安装/升级
|
|
1638
1647
|
|
|
1639
1648
|
def run_mgmt_command(entry, op, cancel_event=None, log_path=None):
|
|
1640
|
-
"""执行 install/upgrade/uninstall 命令(由任务执行器异步运行,日志实时落盘)。"""
|
|
1649
|
+
"""执行 install/upgrade/uninstall 命令(由任务执行器异步运行,日志实时落盘)。"""
|
|
1641
1650
|
if op == "uninstall":
|
|
1642
1651
|
cmd = catalog.uninstall_command(entry)
|
|
1643
1652
|
if not cmd:
|
package/app/core/modelhub.py
CHANGED
|
@@ -2784,6 +2784,7 @@ def test_model(provider_id, model_name, key_id=""):
|
|
|
2784
2784
|
# 这类网关两种 wire 挂在不同路径,同 base 探测必 404,只能按已知映射补候选。
|
|
2785
2785
|
_KNOWN_WIRE_BASES = (
|
|
2786
2786
|
(("api.z.ai",), "/api/anthropic", "https://api.z.ai/api/paas/v4"),
|
|
2787
|
+
(("open.bigmodel.cn",), "/api/anthropic", "https://open.bigmodel.cn/api/paas/v4"),
|
|
2787
2788
|
)
|
|
2788
2789
|
|
|
2789
2790
|
|
package/app/core/prefs.py
CHANGED
|
@@ -1,70 +1,74 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""偏好记忆(借鉴 chinese-novelist-skill 的偏好记忆,2026-09-21):记住用户
|
|
3
|
-
上次创建任务时选择的类型与参数,新建任务时预填「最近一次的选择」而不是
|
|
4
|
-
写死的出厂默认——高频用户不用每次重调轮数/阈值/章节。
|
|
5
|
-
|
|
6
|
-
只记参数不记内容(目标/标题绝不落库);单文件带锁读写,损坏即弃。
|
|
7
|
-
"""
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
import threading
|
|
12
|
-
|
|
13
|
-
from . import paths
|
|
14
|
-
|
|
15
|
-
_LOCK = threading.Lock()
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
""
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""偏好记忆(借鉴 chinese-novelist-skill 的偏好记忆,2026-09-21):记住用户
|
|
3
|
+
上次创建任务时选择的类型与参数,新建任务时预填「最近一次的选择」而不是
|
|
4
|
+
写死的出厂默认——高频用户不用每次重调轮数/阈值/章节。
|
|
5
|
+
|
|
6
|
+
只记参数不记内容(目标/标题绝不落库);单文件带锁读写,损坏即弃。
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import threading
|
|
12
|
+
|
|
13
|
+
from . import paths
|
|
14
|
+
|
|
15
|
+
_LOCK = threading.Lock()
|
|
16
|
+
|
|
17
|
+
SAVE_KEYS = ("type", "rounds", "threshold", "best_of", "chapters",
|
|
18
|
+
"words_per_chapter", "variants")
|
|
19
|
+
_LIMITS = {"rounds": (1, 5), "threshold": (1.0, 10.0), "best_of": (1, 3),
|
|
20
|
+
"chapters": (1, 2000), "words_per_chapter": (200, 20000),
|
|
21
|
+
"variants": (1, 3)}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _file():
|
|
25
|
+
# 动态取路径:DATA_DIR 可被测试重定向,模块级缓存会绑死首次导入的目录
|
|
26
|
+
return paths.DATA_DIR / "last-prefs.json"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _clamp(key, value):
|
|
30
|
+
"""数值钳制:历史文件被手改出天际值时按参数合法域兜底。"""
|
|
31
|
+
lo, hi = _LIMITS.get(key), (0, 10 ** 9)
|
|
32
|
+
lo, hi = lo if lo else hi
|
|
33
|
+
try:
|
|
34
|
+
return max(lo, min(hi, type(lo)(value) if isinstance(lo, int)
|
|
35
|
+
else float(value)))
|
|
36
|
+
except (TypeError, ValueError):
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def record(payload):
|
|
41
|
+
"""从创建任务的 payload 提取可复用参数(存在才覆盖,单键独立保留)。"""
|
|
42
|
+
if not isinstance(payload, dict):
|
|
43
|
+
return
|
|
44
|
+
with _LOCK:
|
|
45
|
+
cur = load()
|
|
46
|
+
for key in SAVE_KEYS:
|
|
47
|
+
v = payload.get(key)
|
|
48
|
+
if v is None:
|
|
49
|
+
continue
|
|
50
|
+
if key == "type":
|
|
51
|
+
v = str(v)[:32]
|
|
52
|
+
if v:
|
|
53
|
+
cur["type"] = v
|
|
54
|
+
continue
|
|
55
|
+
c = _clamp(key, v)
|
|
56
|
+
if c is not None:
|
|
57
|
+
cur[key] = c
|
|
58
|
+
tmp = _file().with_suffix(".tmp")
|
|
59
|
+
try:
|
|
60
|
+
paths.DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
tmp.write_text(json.dumps(cur, ensure_ascii=False, indent=2),
|
|
62
|
+
encoding="utf-8")
|
|
63
|
+
tmp.replace(_file())
|
|
64
|
+
except OSError:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def load():
|
|
69
|
+
"""读最近偏好;文件缺失/损坏返回 {}。"""
|
|
70
|
+
try:
|
|
71
|
+
data = json.loads(_file().read_text(encoding="utf-8"))
|
|
72
|
+
return data if isinstance(data, dict) else {}
|
|
73
|
+
except (OSError, ValueError):
|
|
74
|
+
return {}
|
package/app/core/runner.py
CHANGED
|
@@ -397,15 +397,18 @@ def pretty_cli_log(text, max_event_chars=4000):
|
|
|
397
397
|
return "\n".join(out)
|
|
398
398
|
|
|
399
399
|
|
|
400
|
-
def run_process(argv=None, shell_cmd=None, stdin_text=None, cwd=None, env=None,
|
|
401
|
-
timeout=DEFAULT_TIMEOUT, cancel_event=None, log_path=None,
|
|
402
|
-
stall_timeout=0):
|
|
400
|
+
def run_process(argv=None, shell_cmd=None, stdin_text=None, cwd=None, env=None,
|
|
401
|
+
timeout=DEFAULT_TIMEOUT, cancel_event=None, log_path=None,
|
|
402
|
+
stall_timeout=0, repeat_abort=None):
|
|
403
403
|
"""通用子进程执行:并发读管道防死锁;超时/取消杀整棵进程树。
|
|
404
404
|
|
|
405
405
|
stall_timeout:停滞看门狗(秒,0=关闭)——超过该时长 stdout/stderr 无任何
|
|
406
406
|
新输出即判卡死,提前杀树返回(timed_out=True + stalled=True)。只对输出
|
|
407
|
-
持续流动的 CLI 开(codex JSONL 事件流);claude json 到结束才一次性输出,
|
|
408
|
-
开了会把正常长任务误杀。
|
|
407
|
+
持续流动的 CLI 开(codex JSONL 事件流);claude json 到结束才一次性输出,
|
|
408
|
+
开了会把正常长任务误杀。
|
|
409
|
+
|
|
410
|
+
repeat_abort:可选 (marker, count)。同一致命错误达到次数即提前杀树,处理
|
|
411
|
+
CLI 自身不断打印重连消息、因此永远触发不了静默看门狗的假运行。
|
|
409
412
|
|
|
410
413
|
返回 {ok, exit_code, stdout, stderr, duration, cancelled, timed_out, stalled}。
|
|
411
414
|
"""
|
|
@@ -468,7 +471,7 @@ def run_process(argv=None, shell_cmd=None, stdin_text=None, cwd=None, env=None,
|
|
|
468
471
|
pass
|
|
469
472
|
threading.Thread(target=_feed, daemon=True).start()
|
|
470
473
|
start = time.time()
|
|
471
|
-
cancelled = timed_out = stalled = False
|
|
474
|
+
cancelled = timed_out = stalled = repeat_aborted = False
|
|
472
475
|
while True:
|
|
473
476
|
try:
|
|
474
477
|
proc.wait(timeout=0.4)
|
|
@@ -485,14 +488,27 @@ def run_process(argv=None, shell_cmd=None, stdin_text=None, cwd=None, env=None,
|
|
|
485
488
|
_kill_tree(proc.pid)
|
|
486
489
|
_drain_streams(proc, t_out, t_err, timeout=5)
|
|
487
490
|
break
|
|
488
|
-
if stall_timeout and time.time() - stamp[0] > stall_timeout:
|
|
491
|
+
if stall_timeout and time.time() - stamp[0] > stall_timeout:
|
|
489
492
|
# 停滞看门狗:长静默多为卡死(网关挂起/CLI 假死),与其耗满总
|
|
490
493
|
# 超时不如提前杀——错误按超时归类,走既有的换模型/换将链路
|
|
491
494
|
stalled = True
|
|
492
495
|
timed_out = True
|
|
493
496
|
_kill_tree(proc.pid)
|
|
494
|
-
_drain_streams(proc, t_out, t_err, timeout=5)
|
|
495
|
-
break
|
|
497
|
+
_drain_streams(proc, t_out, t_err, timeout=5)
|
|
498
|
+
break
|
|
499
|
+
if repeat_abort:
|
|
500
|
+
try:
|
|
501
|
+
marker, limit = repeat_abort
|
|
502
|
+
recent = decode_output(b"".join(out_chunks)[-8000:] +
|
|
503
|
+
b"\n" + b"".join(err_chunks)[-8000:])
|
|
504
|
+
if marker and recent.count(str(marker)) >= int(limit):
|
|
505
|
+
repeat_aborted = True
|
|
506
|
+
timed_out = True
|
|
507
|
+
_kill_tree(proc.pid)
|
|
508
|
+
_drain_streams(proc, t_out, t_err, timeout=5)
|
|
509
|
+
break
|
|
510
|
+
except (TypeError, ValueError):
|
|
511
|
+
pass
|
|
496
512
|
duration = round(time.time() - start, 1)
|
|
497
513
|
t_out.join(timeout=5)
|
|
498
514
|
t_err.join(timeout=5)
|
|
@@ -505,9 +521,11 @@ def run_process(argv=None, shell_cmd=None, stdin_text=None, cwd=None, env=None,
|
|
|
505
521
|
stream.close()
|
|
506
522
|
except Exception:
|
|
507
523
|
pass
|
|
508
|
-
if cancelled:
|
|
509
|
-
stderr += "\n[已被用户取消]"
|
|
510
|
-
elif
|
|
524
|
+
if cancelled:
|
|
525
|
+
stderr += "\n[已被用户取消]"
|
|
526
|
+
elif repeat_aborted:
|
|
527
|
+
stderr += "\n[同一网络错误重复 %s 次,已提前终止进程树]" % repeat_abort[1]
|
|
528
|
+
elif stalled:
|
|
511
529
|
stderr += "\n[输出停滞 %ss,已终止进程树]" % stall_timeout
|
|
512
530
|
elif timed_out:
|
|
513
531
|
stderr += "\n[超时 %ss,已终止进程树]" % timeout
|
|
@@ -1050,10 +1068,12 @@ def run_agent(agent, prompt, workdir=None, readonly=True,
|
|
|
1050
1068
|
att["model"], prompt,
|
|
1051
1069
|
images=images if kind == "codex" else None,
|
|
1052
1070
|
workdir=workdir)
|
|
1053
|
-
try:
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1071
|
+
try:
|
|
1072
|
+
repeat_guard = (("Reconnecting... waiting for network", 5)
|
|
1073
|
+
if kind == "codex" else None)
|
|
1074
|
+
res = run_process(argv=argv, stdin_text=stdin_text, cwd=workdir, env=env,
|
|
1075
|
+
timeout=timeout, cancel_event=cancel_event, log_path=log_path,
|
|
1076
|
+
stall_timeout=stall_t, repeat_abort=repeat_guard)
|
|
1057
1077
|
finally:
|
|
1058
1078
|
# 超长指令临时文件:CLI 进程已结束(管道已收),即刻清场不污染工作目录
|
|
1059
1079
|
for tf in tmp_files:
|
|
@@ -1089,10 +1109,11 @@ def run_agent(agent, prompt, workdir=None, readonly=True,
|
|
|
1089
1109
|
parsed_err = _parse_claude_json(res["stdout"] or "")
|
|
1090
1110
|
if parsed_err and parsed_err.get("is_error") and parsed_err.get("text"):
|
|
1091
1111
|
out["error"] = "claude 返回 is_error: " + parsed_err["text"][:500]
|
|
1092
|
-
if kind == "codex" and att.get("provider_id"):
|
|
1093
|
-
el = out["error"].lower()
|
|
1094
|
-
if (("wire_api" in el and "no longer supported" in el)
|
|
1095
|
-
or "no route matched" in el
|
|
1112
|
+
if kind == "codex" and att.get("provider_id"):
|
|
1113
|
+
el = out["error"].lower()
|
|
1114
|
+
if (("wire_api" in el and "no longer supported" in el)
|
|
1115
|
+
or "no route matched" in el
|
|
1116
|
+
or "同一网络错误重复" in out["error"]):
|
|
1096
1117
|
# codex 撞 chat-only 供应商(0.154 只讲 responses):
|
|
1097
1118
|
# 自动冷却该供应商 30 分钟,链展开/路由绑定分随之
|
|
1098
1119
|
# 自动绕开——不用人工改绑定(2026-09-17 mo-so 实测)
|
package/app/ui/app.js
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
const $ = (id) => document.getElementById(id);
|
|
5
|
-
const S = { state: null, catalog: null, catSig: "", providers: null, bindings: null, modelsSig: "", bindSig: "", tab: "tasks", detailRunId: null, pollTimer: null, showArchived: false, /* 会话内开关:每次加载默认隐藏已归档、图标不选中(不持久化,见 btn-side-arch) */ selProvs: {}, selModels: {}, selRuns: {}, bindSel: {}, catalogChecking: false, updateCheckAt: 0, control: null, sseLive: false, es: null, flows: null, orch: null, skills: null, orchSig: "", settings: null, sessionAgents: new Set(), atts: [], gitInfo: null, gitWb: null, gitWbKey: "", gitWbAt: 0, gitWbBusy: false, creatingTask: false, inspKey: null, inspData: null, inspSig: "", inspAt: 0, inspTab: "git", inspAutoSig: "", rdTab: null, rdTabSig: "", rdTabPin: false, _rdCtx: {} };
|
|
5
|
+
const S = { state: null, catalog: null, catSig: "", providers: null, bindings: null, modelsSig: "", bindSig: "", tab: "tasks", detailRunId: null, pollTimer: null, showArchived: false, /* 会话内开关:每次加载默认隐藏已归档、图标不选中(不持久化,见 btn-side-arch) */ selProvs: {}, selModels: {}, selRuns: {}, bindSel: {}, catalogChecking: false, updateCheckAt: 0, control: null, sseLive: false, es: null, flows: null, orch: null, skills: null, orchSig: "", settings: null, sessionAgents: new Set(), atts: [], gitInfo: null, gitWb: null, gitWbKey: "", gitWbAt: 0, gitWbBusy: false, creatingTask: false, inspKey: null, inspData: null, inspSig: "", inspAt: 0, inspTab: "git", inspAutoSig: "", rdTab: null, rdTabSig: "", rdTabPin: false, _rdCtx: {}, lastPrefs: null };
|
|
6
6
|
|
|
7
7
|
/* ---------------------------------------------------------- 任务类型(流程) */
|
|
8
8
|
async function loadFlows() {
|
|
9
|
+
// 偏好记忆(借鉴 chinese-novelist-skill):并行拉最近一次的任务参数,
|
|
10
|
+
// renderTypeOptions/onTypeChange 用它预填类型与轮数/阈值等(拉失败不挡首屏)
|
|
11
|
+
api("/api/prefs").then((r) => { S.lastPrefs = (r && r.prefs) || {}; })
|
|
12
|
+
.then(() => renderTypeOptions())
|
|
13
|
+
.catch(() => {});
|
|
9
14
|
try {
|
|
10
15
|
const r = await api("/api/flows");
|
|
11
16
|
S.flows = r.flows || [];
|
|
@@ -314,7 +319,8 @@ function operationLabel(path, opts) {
|
|
|
314
319
|
const method = String((opts && opts.method) || "GET").toUpperCase();
|
|
315
320
|
if (method === "GET" || method === "HEAD") return "";
|
|
316
321
|
// 只自动记录刚由用户点击触发的写操作;后台维护请求需显式传 operation 才显示。
|
|
317
|
-
if (!(opts && opts.operation === true) &&
|
|
322
|
+
if (!(opts && opts.operation === true) &&
|
|
323
|
+
(!_lastActionAt || performance.now() - _lastActionAt >= 2000)) return "";
|
|
318
324
|
const clean = String(path || "").split("?")[0];
|
|
319
325
|
const rules = [
|
|
320
326
|
[/^\/api\/tasks$/, "创建任务"],
|
|
@@ -8472,7 +8478,7 @@ function renderZentaoProfiles() {
|
|
|
8472
8478
|
const box = $("zt-profiles");
|
|
8473
8479
|
if (!box) return;
|
|
8474
8480
|
box.innerHTML = (S.ztProfiles || []).map((p, i) => ztProfCardHtml(p, i)).join("") ||
|
|
8475
|
-
'<div class="empty">' + t("
|
|
8481
|
+
'<div class="empty">' + t("还没有产品档案——先测试连接,再从禅道选择产品,无需自己查 ID。") + "</div>";
|
|
8476
8482
|
}
|
|
8477
8483
|
|
|
8478
8484
|
/* DOM 卡片 → profiles 数组(结构变化前采集,保住已输入值) */
|
|
@@ -8513,6 +8519,10 @@ function ztProfAdd() {
|
|
|
8513
8519
|
repos: { backend: {}, frontend: {} }, repo_hints: { backend: "", frontend: "" },
|
|
8514
8520
|
owners: { backend: "", frontend: "", not_ours: "" }, module_routes: [] });
|
|
8515
8521
|
renderZentaoProfiles();
|
|
8522
|
+
// 首次添加时自动拉清单,避免用户只能盲填产品 ID;拉不到仍保留手填回退。
|
|
8523
|
+
if (!(S.ztProducts || []).length && $("zt-base-url") && $("zt-base-url").value.trim()) {
|
|
8524
|
+
ztFetchCatalog(false);
|
|
8525
|
+
}
|
|
8516
8526
|
}
|
|
8517
8527
|
|
|
8518
8528
|
function ztProfDel(i) {
|
|
@@ -8643,7 +8653,7 @@ function zentaoOpenRun(runId) {
|
|
|
8643
8653
|
}
|
|
8644
8654
|
|
|
8645
8655
|
/* 表单 → 保存配置。password 只在用户输入了才提交(空 = 保持已存密码)。 */
|
|
8646
|
-
|
|
8656
|
+
function ztFormPayload() {
|
|
8647
8657
|
const val = (id) => (($(id).value || "").trim());
|
|
8648
8658
|
const payload = {
|
|
8649
8659
|
base_url: val("zt-base-url"),
|
|
@@ -8657,6 +8667,11 @@ async function saveZentao() {
|
|
|
8657
8667
|
};
|
|
8658
8668
|
const pw = ($("zt-password").value || "").trim();
|
|
8659
8669
|
if (pw) payload.password = pw;
|
|
8670
|
+
return payload;
|
|
8671
|
+
}
|
|
8672
|
+
|
|
8673
|
+
async function saveZentao() {
|
|
8674
|
+
const payload = ztFormPayload();
|
|
8660
8675
|
try {
|
|
8661
8676
|
await api("/api/zentao/config", { method: "POST", body: JSON.stringify(payload) });
|
|
8662
8677
|
} catch (e) { toast(t("保存失败:") + e.message, true); return; }
|
|
@@ -8682,6 +8697,16 @@ async function testZentao() {
|
|
|
8682
8697
|
toast(t("地址已自动补全子路径,记得「保存配置」"));
|
|
8683
8698
|
}
|
|
8684
8699
|
}
|
|
8700
|
+
if (r.ok) {
|
|
8701
|
+
try {
|
|
8702
|
+
// 测试成功即保存当前连接并拉产品清单,用户无需再理解“先保存再拉取”的顺序。
|
|
8703
|
+
await api("/api/zentao/config", { method: "POST", operation: false,
|
|
8704
|
+
body: JSON.stringify(ztFormPayload()) });
|
|
8705
|
+
await loadZentao();
|
|
8706
|
+
await ztFetchCatalog(true);
|
|
8707
|
+
toast(t("连接成功,已保存;现在可直接从禅道选择产品"));
|
|
8708
|
+
} catch (e) { toast(t("连接成功,但保存配置失败:") + e.message, true); }
|
|
8709
|
+
}
|
|
8685
8710
|
}
|
|
8686
8711
|
|
|
8687
8712
|
async function scanZentao() {
|
|
@@ -9434,6 +9459,9 @@ function renderSu() {
|
|
|
9434
9459
|
const info = $("su-info");
|
|
9435
9460
|
if (!info) return;
|
|
9436
9461
|
if (!SU) { info.textContent = t("无法获取版本信息(服务未连接)"); return; }
|
|
9462
|
+
// 真实内容就绪后摘掉「空状态」皮肤:.empty 是居中+虚线框的加载占位样式,
|
|
9463
|
+
// 版本信息与更新说明都应该左对齐阅读(用户反馈「能不能左对齐」)
|
|
9464
|
+
info.classList.remove("empty");
|
|
9437
9465
|
const modeTxt = t(SU_MODE_TXT[SU.mode] || "未知安装方式");
|
|
9438
9466
|
const cur = SU.current ? "v" + SU.current : t("(未同步版本号)");
|
|
9439
9467
|
let html = "<p class=\"hint\">" + t("当前版本") + t(":") + "<b>" + cur + "</b>" + t(" · ") + t("安装方式") + t(":") + modeTxt + "</p>";
|
package/app/ui/i18n.js
CHANGED
|
@@ -1962,6 +1962,11 @@
|
|
|
1962
1962
|
"AI 排查兜底": "AI triage fallback",
|
|
1963
1963
|
"产品档案": "Product Profiles",
|
|
1964
1964
|
"+ 添加产品": "+ Add product",
|
|
1965
|
+
"+ 从禅道添加产品": "+ Add from ZenTao",
|
|
1966
|
+
"测试并保存连接": "Test & save connection",
|
|
1967
|
+
"刷新产品列表": "Refresh products",
|
|
1968
|
+
"重新从禅道拉取产品与账号": "Reload products and accounts from ZenTao",
|
|
1969
|
+
"先测试连接,再点「从禅道添加产品」直接选择产品;无需自己查 ID。每个产品只需配置我方仓库,负责人和模块路由均可稍后补充。": "Test the connection, then choose Add from ZenTao — no ID lookup needed. Configure only your repository first; owners and module routes can be added later.",
|
|
1965
1970
|
"删除产品": "Remove product",
|
|
1966
1971
|
"每个产品一份档案:我方端(自动修)、两端仓库与负责人、模块路由(模块 → 端/人,排查优先级最高)。": "One profile per product: our sides (auto-fix), frontend/backend repos and owners, module routes (module → side/owner, highest triage priority).",
|
|
1967
1972
|
"定时扫描禅道多个产品的激活 Bug:先按模块路由/AI 排查定责(前端/后端/双端/非我方),我方端自动创建代码修复任务(实现→验证→评审→修复),修完自动合并并 resolve 或转派对应负责人;测试指错人也按排查结论改派。": "Periodically scans active bugs across products: triages each (frontend/backend/both/not-ours) via module routes or AI, auto-fixes on our sides (implement → verify → review → fix), then merges & resolves or transfers to the right owner — including re-routing bugs the tester assigned to the wrong person.",
|
|
@@ -1999,6 +2004,7 @@
|
|
|
1999
2004
|
"需人工": "Needs human",
|
|
2000
2005
|
"还没有认领过 Bug——配置好连接与产品档案后点「立即扫描」。": "No bugs claimed yet — set up connection and product profiles, then click \"Scan Now\".",
|
|
2001
2006
|
"还没有产品档案——点「+ 添加产品」,填产品 ID 与两端仓库。": "No product profiles yet — click \"+ Add product\" and fill the product ID and repos.",
|
|
2007
|
+
"还没有产品档案——先测试连接,再从禅道选择产品,无需自己查 ID。": "No product profiles yet — test the connection, then select a product from ZenTao; no ID lookup needed.",
|
|
2002
2008
|
"先填产品 ID 再拉模块清单": "Fill the product ID before fetching modules",
|
|
2003
2009
|
"先选产品再拉模块清单": "Pick a product before fetching modules",
|
|
2004
2010
|
"拉取失败": "Fetch failed",
|
package/app/ui/index.html
CHANGED
|
@@ -544,7 +544,7 @@
|
|
|
544
544
|
<h2 data-i18n="禅道 Bug 自动修复">禅道 Bug 自动修复</h2>
|
|
545
545
|
<button class="hhelp" data-help-topic="auto" title="这是什么?点开看帮助" data-i18n-title="这是什么?点开看帮助" aria-label="帮助" data-i18n-aria="帮助"><svg class="ico" aria-hidden="true"><use href="#i-help"></use></svg></button>
|
|
546
546
|
<div class="actions">
|
|
547
|
-
<button id="btn-zentao-test" class="ghost small" title="
|
|
547
|
+
<button id="btn-zentao-test" class="ghost small" title="测试成功后自动保存连接并拉取产品列表" data-i18n-title="测试成功后自动保存连接并拉取产品列表"><span data-i18n="测试并保存连接">测试并保存连接</span></button>
|
|
548
548
|
<button id="btn-zentao-scan" class="primary small" title="立刻扫一遍激活 Bug 并对账回写" data-i18n-title="立刻扫一遍激活 Bug 并对账回写"><span data-i18n="立即扫描">立即扫描</span></button>
|
|
549
549
|
</div>
|
|
550
550
|
</div>
|
|
@@ -566,11 +566,11 @@
|
|
|
566
566
|
</div>
|
|
567
567
|
<div class="ops"><button id="btn-zentao-save" class="primary small"><span data-i18n="保存配置">保存配置</span></button></div>
|
|
568
568
|
<h3 data-i18n="产品档案" class="sec-title">产品档案</h3>
|
|
569
|
-
<p data-i18n="
|
|
569
|
+
<p data-i18n="先测试连接,再点「从禅道添加产品」直接选择产品;无需自己查 ID。每个产品只需配置我方仓库,负责人和模块路由均可稍后补充。" class="hint">先测试连接,再点「从禅道添加产品」直接选择产品;无需自己查 ID。每个产品只需配置我方仓库,负责人和模块路由均可稍后补充。</p>
|
|
570
570
|
<div id="zt-profiles" class="cards"></div>
|
|
571
571
|
<div class="ops">
|
|
572
|
-
<button id="btn-zentao-fetchcat" class="ghost small" title="
|
|
573
|
-
<button id="btn-zentao-addprofile" class="
|
|
572
|
+
<button id="btn-zentao-fetchcat" class="ghost small" title="重新从禅道拉取产品与账号" data-i18n-title="重新从禅道拉取产品与账号"><span data-i18n="刷新产品列表">刷新产品列表</span></button>
|
|
573
|
+
<button id="btn-zentao-addprofile" class="primary small"><span data-i18n="+ 从禅道添加产品">+ 从禅道添加产品</span></button>
|
|
574
574
|
</div>
|
|
575
575
|
<datalist id="zt-users"></datalist>
|
|
576
576
|
<h3 data-i18n="修复记录" class="sec-title">修复记录</h3>
|
package/app/ui/style.css
CHANGED
|
@@ -5508,3 +5508,16 @@ button.request-busy > * { opacity: .18; }
|
|
|
5508
5508
|
@media (prefers-reduced-motion: reduce) {
|
|
5509
5509
|
.op-pill.running .ico, .op-row.running .op-state { animation: none; }
|
|
5510
5510
|
}
|
|
5511
|
+
|
|
5512
|
+
/* 关于与更新:版本信息面板常态样式(加载中才挂 .empty 居中虚线皮肤;
|
|
5513
|
+
renderSu 填充内容后移除 empty,改用这里的左对齐阅读样式——用户反馈) */
|
|
5514
|
+
#su-info {
|
|
5515
|
+
padding: 12px 14px;
|
|
5516
|
+
border: 1px solid var(--border, rgba(128, 128, 128, .25));
|
|
5517
|
+
border-radius: 8px;
|
|
5518
|
+
background: var(--bg-soft, rgba(128, 128, 128, .06));
|
|
5519
|
+
text-align: left;
|
|
5520
|
+
font-size: 13px;
|
|
5521
|
+
line-height: 1.6;
|
|
5522
|
+
}
|
|
5523
|
+
#su-info.empty { text-align: center; }
|
package/package.json
CHANGED