@sakanano-moe/agenthelm 6.7.0 → 6.11.0-rc.1

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.
Files changed (86) hide show
  1. package/package.json +1 -1
  2. package/runtime/index.json +6 -6
  3. package/runtime/plugins/agent-pack/.codex-plugin/plugin.json +1 -1
  4. package/runtime/plugins/agent-pack/hooks/hooks.json +32 -0
  5. package/runtime/plugins/agent-pack/plugin.json +1 -1
  6. package/runtime/plugins/agent-pack/references/local-only.md +126 -0
  7. package/runtime/plugins/agent-pack/references/nudge.en.md +58 -0
  8. package/runtime/plugins/agent-pack/references/nudge.md +58 -0
  9. package/runtime/plugins/agent-pack/runtime/agent_pack_nudge.py +573 -0
  10. package/runtime/plugins/agent-pack/runtime/agenthelm.py +419 -107
  11. package/runtime/plugins/agent-pack/runtime/agenthelm_runtime.py +77 -53
  12. package/runtime/plugins/agent-pack/runtime/i18n_agent_pack.py +78 -0
  13. package/runtime/plugins/agent-pack/runtime/local_only.py +1130 -0
  14. package/runtime/plugins/agent-pack/runtime/locale/en.json +385 -0
  15. package/runtime/plugins/agent-pack/runtime/mcp_server.py +25 -0
  16. package/runtime/plugins/agent-pack/runtime/nudge_text.py +107 -0
  17. package/runtime/plugins/agent-pack/skills/agent-pack/SKILL.md +9 -1
  18. package/runtime/plugins/agents-init/.codex-plugin/plugin.json +1 -1
  19. package/runtime/plugins/agents-init/content.json +17 -2
  20. package/runtime/plugins/agents-init/plugin.json +1 -1
  21. package/runtime/plugins/agents-init/skills/agents-init/AGENT.template.md +47 -0
  22. package/runtime/plugins/agents-init/skills/agents-init/SKILL.md +39 -1
  23. package/runtime/plugins/agents-init/skills/agents-init/project.schema.json +404 -70
  24. package/runtime/plugins/agents-init/skills/agents-init/scripts/adopt.py +596 -0
  25. package/runtime/plugins/agents-init/skills/agents-init/scripts/i18n_init.py +78 -0
  26. package/runtime/plugins/agents-init/skills/agents-init/scripts/init_project.py +245 -72
  27. package/runtime/plugins/agents-init/skills/agents-init/scripts/locale/en.json +210 -0
  28. package/runtime/plugins/agents-init/skills/agents-init/scripts/regions.py +99 -0
  29. package/runtime/plugins/agents-init/skills/agents-init/scripts/scaffold.py +1081 -0
  30. package/runtime/plugins/agents-init/skills/agents-init/scripts/test_adopt.py +551 -0
  31. package/runtime/plugins/agents-init/skills/agents-init/scripts/test_init_project.py +46 -2
  32. package/runtime/plugins/agents-init/skills/agents-init/scripts/test_scaffold.py +555 -0
  33. package/runtime/plugins/agents-init/skills/agents-init/skeleton.json +66 -0
  34. package/runtime/plugins/agents-init/skills/agents-init/skill.json +1 -1
  35. package/runtime/plugins/opinion-manager/.codex-plugin/plugin.json +1 -1
  36. package/runtime/plugins/opinion-manager/plugin.json +1 -1
  37. package/runtime/plugins/opinion-manager/references/inbox.en.md +48 -0
  38. package/runtime/plugins/opinion-manager/references/inbox.md +48 -0
  39. package/runtime/plugins/opinion-manager/runtime/context.py +19 -18
  40. package/runtime/plugins/opinion-manager/runtime/i18n_opinion.py +78 -0
  41. package/runtime/plugins/opinion-manager/runtime/inbox.py +397 -0
  42. package/runtime/plugins/opinion-manager/runtime/inbox_text.py +101 -0
  43. package/runtime/plugins/opinion-manager/runtime/lifecycle.py +45 -44
  44. package/runtime/plugins/opinion-manager/runtime/locale/en.json +184 -0
  45. package/runtime/plugins/opinion-manager/runtime/opinion_manager.py +55 -50
  46. package/runtime/plugins/opinion-manager/runtime/versioned.py +84 -83
  47. package/runtime/plugins/opinion-manager/skills/opinion-manager/SKILL.md +15 -2
  48. package/runtime/plugins/project-orchestrator/.codex-plugin/plugin.json +1 -1
  49. package/runtime/plugins/project-orchestrator/content.json +30 -3
  50. package/runtime/plugins/project-orchestrator/plugin.json +1 -1
  51. package/runtime/plugins/project-orchestrator/references/case-workspaces.md +9 -1
  52. package/runtime/plugins/project-orchestrator/references/case.schema.json +1 -0
  53. package/runtime/plugins/project-orchestrator/references/sop-interview.md +83 -0
  54. package/runtime/plugins/project-orchestrator/references/sop.md +156 -0
  55. package/runtime/plugins/project-orchestrator/runtime/assets.py +52 -57
  56. package/runtime/plugins/project-orchestrator/runtime/case.py +139 -80
  57. package/runtime/plugins/project-orchestrator/runtime/case_structure.py +71 -0
  58. package/runtime/plugins/project-orchestrator/runtime/case_workspace.py +47 -46
  59. package/runtime/plugins/project-orchestrator/runtime/i18n_orchestrator.py +78 -0
  60. package/runtime/plugins/project-orchestrator/runtime/locale/en.json +1409 -0
  61. package/runtime/plugins/project-orchestrator/runtime/memory.py +44 -43
  62. package/runtime/plugins/project-orchestrator/runtime/private_paths.py +119 -0
  63. package/runtime/plugins/project-orchestrator/runtime/project_base.py +37 -19
  64. package/runtime/plugins/project-orchestrator/runtime/project_cli.py +267 -174
  65. package/runtime/plugins/project-orchestrator/runtime/project_demo.py +130 -48
  66. package/runtime/plugins/project-orchestrator/runtime/project_maintain.py +524 -134
  67. package/runtime/plugins/project-orchestrator/runtime/project_menu.py +159 -85
  68. package/runtime/plugins/project-orchestrator/runtime/project_real.py +191 -106
  69. package/runtime/plugins/project-orchestrator/runtime/project_write.py +58 -55
  70. package/runtime/plugins/project-orchestrator/runtime/sop.py +1670 -0
  71. package/runtime/plugins/project-orchestrator/runtime/sop_structure.py +226 -0
  72. package/runtime/plugins/project-orchestrator/runtime/wf.py +74 -70
  73. package/runtime/plugins/project-orchestrator/skills/project-orchestrator/SKILL.md +22 -0
  74. package/runtime/plugins/project-orchestrator/sop-templates/blank/SOP.en.md +57 -0
  75. package/runtime/plugins/project-orchestrator/sop-templates/blank/SOP.md +57 -0
  76. package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/SOP.en.md +77 -0
  77. package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/SOP.md +77 -0
  78. package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/templates/digest.en.md +11 -0
  79. package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/templates/digest.md +11 -0
  80. package/runtime/plugins/project-orchestrator/sop-templates/video-to-transcript/SOP.en.md +87 -0
  81. package/runtime/plugins/project-orchestrator/sop-templates/video-to-transcript/SOP.md +87 -0
  82. package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/SOP.en.md +75 -0
  83. package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/SOP.md +75 -0
  84. package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/templates/review.en.md +11 -0
  85. package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/templates/review.md +12 -0
  86. package/runtime/plugins/project-orchestrator/templates/CASE.en.md +39 -0
@@ -3,14 +3,27 @@
3
3
  用来在没有项目时体验菜单,也是界面测试的固定数据。数据形状与真实来源(project_real)
4
4
  一致:资料结果沿用 ``assets.find`` 的字段,事项沿用 ``case.py`` 的 Frontmatter 字段。所有
5
5
  “执行”都只返回模拟结果,并带 ``simulated: True``。
6
+
7
+ 流程(SOP)的演示不同:它直接运行真实的 sop.py,但项目、SOP 与 Case 都在一个用完即删的
8
+ 临时目录里(进程结束自动清理),不碰任何真实项目文件。唯一读取真实环境的是“执行前检查”:
9
+ 它看的是这台机器上有没有对应的命令与环境变量,这正是它要演示的东西。
6
10
  """
7
11
  from __future__ import annotations
8
12
 
13
+ import contextlib
9
14
  import copy
15
+ import json
16
+ import os
10
17
  import re
18
+ import shutil
19
+ import tempfile
20
+ import weakref
21
+ from pathlib import Path
11
22
  from typing import Any
12
23
 
13
- from project_base import EntryError
24
+ import sop_structure # noqa: E402
25
+ from project_base import EntryError, sop_call
26
+ from i18n_orchestrator import _, resolve_language # noqa: E402
14
27
 
15
28
  DEMO_NOW = "2026-10-25T09:12:00+08:00"
16
29
  FAULTS = ("index-broken", "offline", "runtime-old", "conflict")
@@ -264,6 +277,34 @@ DEFAULT_GROUPS = {"active": "active", "waiting": "waiting", "captured": "capture
264
277
  DemoError = EntryError
265
278
 
266
279
 
280
+ # 演示数据本身是中文原文;按当前语言整棵翻译。旧编号、编号与路径是用来匹配和查找的标识,保持原样;
281
+ # 项目自定义的状态词(status 与 status_map 的键)一起翻译,两边保持对应。
282
+ KEEP_KEYS = {"legacy_ids", "id", "path", "command", "launcher"}
283
+
284
+
285
+ def translate_tree(value: Any, key: str | None = None) -> Any:
286
+ if isinstance(value, dict):
287
+ return {(_(item_key) if key == "status_map" and isinstance(item_key, str) else item_key):
288
+ item if item_key in KEEP_KEYS else translate_tree(item, item_key) for item_key, item in value.items()}
289
+ if isinstance(value, (list, tuple)):
290
+ return type(value)(translate_tree(item) for item in value)
291
+ return _(value) if isinstance(value, str) else value
292
+
293
+
294
+ @contextlib.contextmanager
295
+ def _sop_home(root: Path):
296
+ """演示期间把用户级 SOP 目录指到临时目录,结束后原样恢复环境变量。"""
297
+ previous = os.environ.get("AGENTHELM_SOP_HOME")
298
+ os.environ["AGENTHELM_SOP_HOME"] = str(root.parent / "user-sops")
299
+ try:
300
+ yield
301
+ finally:
302
+ if previous is None:
303
+ os.environ.pop("AGENTHELM_SOP_HOME", None)
304
+ else:
305
+ os.environ["AGENTHELM_SOP_HOME"] = previous
306
+
307
+
267
308
  class DemoProvider:
268
309
  """内存里的虚构项目;所有结果带 demo 标记,所有写入只模拟。"""
269
310
 
@@ -271,11 +312,11 @@ class DemoProvider:
271
312
 
272
313
  def __init__(self, scenario: str, fault: str | None = None) -> None:
273
314
  if scenario not in SCENARIOS:
274
- raise DemoError(f"没有这个演示场景:{scenario}", "usage", [f"可选:{'、'.join(SCENARIOS)}"])
315
+ raise DemoError(_("没有这个演示场景:{0}").format(scenario), "usage", [_("可选:{0}").format(_('、').join(SCENARIOS))])
275
316
  if fault is not None and fault not in FAULTS:
276
- raise DemoError(f"没有这个演示故障:{fault}", "usage", [f"可选:{'、'.join(FAULTS)}"])
317
+ raise DemoError(_("没有这个演示故障:{0}").format(fault), "usage", [_("可选:{0}").format(_('、').join(FAULTS))])
277
318
  self.scenario_id = scenario
278
- self.data = copy.deepcopy(SCENARIOS[scenario])
319
+ self.data = translate_tree(copy.deepcopy(SCENARIOS[scenario]))
279
320
  self.fault = fault
280
321
  titles = {item["id"]: item["title"] for item in self.data["items"]}
281
322
  for asset in self.data["assets"]:
@@ -283,6 +324,45 @@ class DemoProvider:
283
324
  self.name = self.data["name"]
284
325
  self.entry_api = 2 if fault == "runtime-old" else 1
285
326
  self.resolved: set[str] = set() # fixes simulated in this session
327
+ self._sandbox: Path | None = None
328
+
329
+ # -------------------------------------------------------------- 流程(SOP):临时目录里的真实代码
330
+
331
+ def sop(self, argv: list[str]) -> tuple[int, dict, list[str]]:
332
+ root = self._sop_sandbox()
333
+ with _sop_home(root):
334
+ return sop_call(argv, root)
335
+
336
+ def _sop_sandbox(self) -> Path:
337
+ if self._sandbox is not None:
338
+ return self._sandbox / "project"
339
+ self._sandbox = Path(tempfile.mkdtemp(prefix="agenthelm-demo-"))
340
+ weakref.finalize(self, shutil.rmtree, self._sandbox, True) # 进程结束或对象回收时清理
341
+ root = self._sandbox / "project"
342
+ profile = root / ".agents" / "moe.sakanano.agent-pack" / "project.json"
343
+ profile.parent.mkdir(parents=True)
344
+ # 演示项目的文件语言跟着当前界面语言:演示本身就是按界面语言讲的故事
345
+ profile.write_text(json.dumps({"schema_version": "1.0", "name": self.name, "language": resolve_language(), "sop": {"root": "sops"},
346
+ "work": {"mode": "case-workspace", "case": {"root": "cases"}}}), encoding="utf-8")
347
+ if self.scenario_id != "blank":
348
+ self._seed_sops(root)
349
+ return root
350
+
351
+ def _seed_sops(self, root: Path) -> None:
352
+ """三个已启用的样例流程:让 list / preflight / run / step 马上有东西可看。"""
353
+ habits = ("--set", "on_error=ask", "--set", "on_missing_tool=ask", "--set", "artifact_keep=ask",
354
+ "--set", "resume=from-failed-step", "--set", "confirm_costly=ask")
355
+ with _sop_home(root):
356
+ sop_call(["defaults", *habits, "--apply"], root)
357
+ for slug in ("knowledge-digest", "weekly-review", "video-to-transcript"):
358
+ sop_call(["new", "--slug", slug, "--template", slug, "--apply"], root)
359
+ path = root / "sops" / slug / "SOP.md"
360
+ text = path.read_text(encoding="utf-8")
361
+ for marker in sop_structure.PLACEHOLDERS:
362
+ text = text.replace(marker, _("演示"))
363
+ path.write_text(text, encoding="utf-8")
364
+ sop_call(["decide", slug, "--confirm", "trigger", "--confirm", "requires", "--confirm", "scope", "--apply"], root)
365
+ sop_call(["activate", slug, "--apply"], root)
286
366
 
287
367
  # -------------------------------------------------------------- overview
288
368
 
@@ -302,7 +382,7 @@ class DemoProvider:
302
382
  levels: dict[str, int] = {}
303
383
  for finding in self.health()["findings"]:
304
384
  levels[finding["level"]] = levels.get(finding["level"], 0) + 1
305
- return {"name": self.data["name"], "kind": self.data["kind"], "kind_label": self.data["kind"] + "项目",
385
+ return {"name": self.data["name"], "kind": self.data["kind"], "kind_label": _("{0}项目").format(self.data["kind"]),
306
386
  "launcher": self.data["launcher"],
307
387
  "index": self.index_state(), "items": counts, "health": levels, "read_at": DEMO_NOW,
308
388
  "tools": {"declared": len(self.data["tools"]),
@@ -313,8 +393,8 @@ class DemoProvider:
313
393
  def find(self, terms: list[str], *, ext: str | None = None, container: str | None = None,
314
394
  limit: int = 10) -> dict:
315
395
  if self.index_state()["status"] == "broken":
316
- raise DemoError("资料索引损坏,暂时无法查找", "index-broken",
317
- ["重建索引:只改用户缓存里的索引,不改项目文件(5 修复与更新 → 重建索引,或 fix rebuild-index --apply)"])
396
+ raise DemoError(_("资料索引损坏,暂时无法查找"), "index-broken",
397
+ [_("重建索引:只改用户缓存里的索引,不改项目文件(5 修复与更新 → 重建索引,或 fix rebuild-index --apply)")])
318
398
  results = []
319
399
  for asset in self.data["assets"]:
320
400
  haystack = " ".join([asset["path"], asset["title"], asset["case_title"], asset["modified"]]).lower()
@@ -333,9 +413,9 @@ class DemoProvider:
333
413
  return {"path": path, "inbound": known.get("inbound", []), "outbound": known.get("outbound", [])}
334
414
 
335
415
  def act(self, action: str, path: str) -> dict:
336
- verbs = {"open": "打开", "reveal": "在文件管理器中显示", "copy": "复制路径"}
416
+ verbs = {"open": _("打开"), "reveal": _("在文件管理器中显示"), "copy": _("复制路径")}
337
417
  return {"action": action, "path": path, "simulated": True,
338
- "message": f"【演示】将{verbs.get(action, action)} {path}(未实际执行)"}
418
+ "message": _("【演示】将{0} {1}(未实际执行)").format(verbs.get(action, action), path)}
339
419
 
340
420
  # -------------------------------------------------------------- progress
341
421
 
@@ -354,9 +434,9 @@ class DemoProvider:
354
434
  exact = [item for item in self.data["items"] if token in (item["id"], *item["legacy_ids"], *item["aliases"])]
355
435
  matches = exact or [item for item in self.data["items"] if _item_hits(token, item)]
356
436
  if not matches:
357
- raise DemoError(f"没有找到事项:{token}", "not-found", ["试试更短的词,或在「看进展」里浏览全部"])
437
+ raise DemoError(_("没有找到事项:{0}").format(token), "not-found", [_("试试更短的词,或在「看进展」里浏览全部")])
358
438
  if len(matches) > 1:
359
- raise DemoError(f"有 {len(matches)} 个事项都匹配“{token}”", "ambiguous",
439
+ raise DemoError(_("有 {0} 个事项都匹配“{1}”").format(len(matches), token), "ambiguous",
360
440
  [f"{item['id']} {item['title']}" for item in matches])
361
441
  item = matches[0]
362
442
  return {**item, "group": self._group(item), "source": self.data["items_source"]}
@@ -372,29 +452,31 @@ class DemoProvider:
372
452
  index = self.index_state()["status"]
373
453
  record = self.data["record"]
374
454
  return [
375
- {"id": "find", "title": "找资料", "state": "unavailable" if index == "broken" else "available",
376
- "reason": "资料索引损坏,可重建" if index == "broken" else "", "offline": True},
377
- {"id": "progress", "title": "看进展", "state": "available", "reason": "", "offline": True},
378
- {"id": "guide", "title": "项目指南", "state": "available", "reason": "", "offline": True},
379
- {"id": "health", "title": "检查项目与环境", "state": "available",
380
- "reason": "联网项使用缓存" if self.fault == "offline" else "", "offline": True},
381
- {"id": "fix", "title": "修复与更新", "state": "available",
382
- "reason": "离线:需要联网的更新只能预览" if self.fault == "offline" else "", "offline": False},
383
- {"id": "record", "title": "记录事项", "state": "available" if record["write"] else "unavailable",
384
- "reason": "" if record["write"] else "项目工具没有写接口", "offline": True},
385
- {"id": "tools", "title": "项目工具",
455
+ {"id": "find", "title": _("找资料"), "state": "unavailable" if index == "broken" else "available",
456
+ "reason": _("资料索引损坏,可重建") if index == "broken" else "", "offline": True},
457
+ {"id": "progress", "title": _("看进展"), "state": "available", "reason": "", "offline": True},
458
+ {"id": "guide", "title": _("项目指南"), "state": "available", "reason": "", "offline": True},
459
+ {"id": "health", "title": _("检查项目与环境"), "state": "available",
460
+ "reason": _("联网项使用缓存") if self.fault == "offline" else "", "offline": True},
461
+ {"id": "fix", "title": _("修复与更新"), "state": "available",
462
+ "reason": _("离线:需要联网的更新只能预览") if self.fault == "offline" else "", "offline": False},
463
+ {"id": "record", "title": _("记录事项"), "state": "available" if record["write"] else "unavailable",
464
+ "reason": "" if record["write"] else _("项目工具没有写接口"), "offline": True},
465
+ {"id": "tools", "title": _("项目工具"),
386
466
  "state": "available" if self.data["tools"] else "unavailable",
387
- "reason": "" if self.data["tools"] else "项目没有声明专用工具", "offline": True},
467
+ "reason": "" if self.data["tools"] else _("项目没有声明专用工具"), "offline": True},
468
+ {"id": "sop", "title": _("流程(SOP)"), "state": "available", "offline": True,
469
+ "reason": _("演示:在临时目录里运行,结束后自动清理")},
388
470
  ]
389
471
 
390
472
  def health(self) -> dict:
391
473
  findings = [finding for finding in copy.deepcopy(self.data["health"])
392
474
  if not any(step["action"] == f"fix:{fix_id}" for step in finding["next"] for fix_id in self.resolved)]
393
475
  if self.index_state()["status"] == "broken":
394
- findings.insert(0, {"id": "rebuild-index", "level": "must", "area": "资料", "title": "资料索引损坏",
395
- "reason": "缓存里的索引文件无法读取(可能是上次重建被中断)",
396
- "impact": "「找资料」暂时不可用;项目文件本身没有受影响",
397
- "next": [{"label": "重建索引(只改用户缓存)", "action": "fix:rebuild-index"}]})
476
+ findings.insert(0, {"id": "rebuild-index", "level": "must", "area": _("资料"), "title": _("资料索引损坏"),
477
+ "reason": _("缓存里的索引文件无法读取(可能是上次重建被中断)"),
478
+ "impact": _("「找资料」暂时不可用;项目文件本身没有受影响"),
479
+ "next": [{"label": _("重建索引(只改用户缓存)"), "action": "fix:rebuild-index"}]})
398
480
  online = {"status": "offline", "cached_at": "2026-10-22T09:00:00+08:00"} if self.fault == "offline" \
399
481
  else {"status": "online", "cached_at": DEMO_NOW}
400
482
  return {"findings": findings, "ok": self.data["ok"], "read_at": DEMO_NOW, "update_source": online}
@@ -405,20 +487,20 @@ class DemoProvider:
405
487
  fixes = {key: value for key, value in self.data["fixes"].items() if key not in self.resolved}
406
488
  if self.index_state()["status"] == "broken":
407
489
  fixes["rebuild-index"] = {
408
- "title": "重建资料索引", "kind": "index", "network": False,
409
- "changes": ["删除损坏的索引并按当前文件重新登记"],
410
- "writes": [{"path": "~/.cache/agenthelm/assets/<项目摘要>/", "scope": "用户缓存"}],
411
- "custom": [], "breaking": False, "recovery": "不涉及项目文件;索引可随时再次重建",
412
- "verify": "重新读取索引状态:最新,文件数与项目一致"}
490
+ "title": _("重建资料索引"), "kind": "index", "network": False,
491
+ "changes": [_("删除损坏的索引并按当前文件重新登记")],
492
+ "writes": [{"path": _("~/.cache/agenthelm/assets/<项目摘要>/"), "scope": _("用户缓存")}],
493
+ "custom": [], "breaking": False, "recovery": _("不涉及项目文件;索引可随时再次重建"),
494
+ "verify": _("重新读取索引状态:最新,文件数与项目一致")}
413
495
  chosen = ids or sorted(fixes)
414
496
  unknown = [fix_id for fix_id in chosen if fix_id not in fixes]
415
497
  if unknown:
416
- raise DemoError("没有这些可处理项:" + "、".join(unknown), "not-found",
417
- ["可处理:" + ("、".join(sorted(fixes)) or "(无)")])
498
+ raise DemoError(_("没有这些可处理项:") + _("、").join(unknown), "not-found",
499
+ [_("可处理:") + (_("、").join(sorted(fixes)) or _("(无)"))])
418
500
  steps = []
419
501
  for fix_id in chosen:
420
502
  step = {"id": fix_id, **copy.deepcopy(fixes[fix_id])}
421
- step["blocked"] = "离线:需要联网,只能预览" if step["network"] and self.fault == "offline" else ""
503
+ step["blocked"] = _("离线:需要联网,只能预览") if step["network"] and self.fault == "offline" else ""
422
504
  steps.append(step)
423
505
  return {"steps": steps, "simulated": True, "update_source": self.health()["update_source"]}
424
506
 
@@ -441,12 +523,12 @@ class DemoProvider:
441
523
  def record_preview(self, target: str | None, change: dict) -> dict:
442
524
  record = self.data["record"]
443
525
  if not record["write"]:
444
- raise DemoError("这个项目不能在本入口记录事项", "unavailable",
445
- [record["gap"], "需要时请直接使用项目工具,或请项目维护者补充写接口"])
526
+ raise DemoError(_("这个项目不能在本入口记录事项"), "unavailable",
527
+ [record["gap"], _("需要时请直接使用项目工具,或请项目维护者补充写接口")])
446
528
  if target is None:
447
529
  slug = re.sub(r"[^a-z0-9]+", "-", (change.get("slug") or "new-item").lower()).strip("-") or "new-item"
448
530
  path = f"cases/CASE-20261025-001-{slug}/CASE.md"
449
- lines = [f"+ 新建 {path}", f"+ title: {change.get('title', '')}", "+ status: active",
531
+ lines = [_("+ 新建 {0}").format(path), f"+ title: {change.get('title', '')}", "+ status: active",
450
532
  f"+ goal: {change.get('goal', '')}", f"+ next_action: {change.get('next_action', '')}"]
451
533
  return {"target": None, "path": path, "lines": lines, "expect_updated_at": None, "simulated": True}
452
534
  item = self.item(target)
@@ -455,22 +537,22 @@ class DemoProvider:
455
537
  if change.get(key) is not None:
456
538
  lines += [f"- {label}: {item.get(key) or ''}", f"+ {label}: {change[key]}"]
457
539
  if change.get("log"):
458
- lines.append(f"+ 工作记录:- 2026-10-25T09:15 {change['log']}")
540
+ lines.append(_("+ 工作记录:- 2026-10-25T09:15 {0}").format(change['log']))
459
541
  for path in change.get("add_deliverable") or []:
460
542
  lines.append(f"+ deliverables: {path}")
461
543
  if not lines:
462
- raise DemoError("没有要修改的内容", "usage", ["至少给出一项:记录、下一动作、等待对象或状态"])
544
+ raise DemoError(_("没有要修改的内容"), "usage", [_("至少给出一项:记录、下一动作、等待对象或状态")])
463
545
  lines.append(f" updated_at: {item['updated_at']} → 2026-10-25T09:15:00+08:00")
464
546
  return {"target": item["id"], "path": item["path"], "lines": lines,
465
547
  "expect_updated_at": item["updated_at"], "simulated": True}
466
548
 
467
549
  def record_apply(self, preview: dict) -> dict:
468
550
  if self.fault == "conflict" and preview["target"]:
469
- raise DemoError("这个事项在你预览之后被修改过,没有写入", "conflict",
470
- ["最新修改:2026-10-25T09:14(CASE.md 的 updated_at 已变化)",
471
- "重新打开预览,确认对方的改动后再提交"])
551
+ raise DemoError(_("这个事项在你预览之后被修改过,没有写入"), "conflict",
552
+ [_("最新修改:2026-10-25T09:14(CASE.md 的 updated_at 已变化)"),
553
+ _("重新打开预览,确认对方的改动后再提交")])
472
554
  return {"path": preview["path"], "simulated": True,
473
- "verify": "【演示】写入后重新读取:内容与预览一致,未知字段与原有正文保留"}
555
+ "verify": _("【演示】写入后重新读取:内容与预览一致,未知字段与原有正文保留")}
474
556
 
475
557
  # -------------------------------------------------------------- tools
476
558
 
@@ -480,11 +562,11 @@ class DemoProvider:
480
562
  def run_tool(self, tool_id: str) -> dict:
481
563
  tool = next((tool for tool in self.data["tools"] if tool["id"] == tool_id), None)
482
564
  if tool is None:
483
- raise DemoError(f"项目没有声明这个工具:{tool_id}", "not-found")
565
+ raise DemoError(_("项目没有声明这个工具:{0}").format(tool_id), "not-found")
484
566
  if tool["state"] != "available":
485
- raise DemoError(f"“{tool['title']}”当前不可用", "unavailable", [tool["reason"]])
567
+ raise DemoError(_("“{0}”当前不可用").format(tool['title']), "unavailable", [tool["reason"]])
486
568
  return {"id": tool_id, "simulated": True,
487
- "message": f"【演示】将运行:{' '.join(tool['command'])}(未实际执行)"}
569
+ "message": _("【演示】将运行:{0}(未实际执行)").format(' '.join(tool['command']))}
488
570
 
489
571
 
490
572
  def _term_hits(term: str, haystack: str) -> bool:
@@ -501,4 +583,4 @@ def _item_hits(term: str, item: dict) -> bool:
501
583
 
502
584
 
503
585
  def describe() -> list[dict[str, Any]]:
504
- return [{"id": key, "name": value["name"], "kind": value["kind"]} for key, value in SCENARIOS.items()]
586
+ return [{"id": key, "name": _(value["name"]), "kind": _(value["kind"])} for key, value in SCENARIOS.items()]