@sakanano-moe/agenthelm 6.5.3 → 6.6.0
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/bin/agenthelm.js +1 -1
- package/package.json +2 -2
- package/runtime/index.json +80 -0
- package/runtime/plugins/agent-pack/.codex-plugin/plugin.json +19 -0
- package/runtime/plugins/agent-pack/.mcp.json +12 -0
- package/runtime/plugins/agent-pack/hooks/hooks.json +16 -0
- package/runtime/plugins/agent-pack/mcp.json +14 -0
- package/runtime/plugins/agent-pack/plugin.json +20 -0
- package/runtime/plugins/agent-pack/references/capability-config.md +124 -0
- package/runtime/{agenthelm.py → plugins/agent-pack/runtime/agenthelm.py} +42 -1
- package/runtime/plugins/agent-pack/runtime/agenthelm_runtime.py +388 -0
- package/runtime/plugins/agent-pack/skills/agent-pack/SKILL.md +46 -0
- package/runtime/plugins/agents-init/.codex-plugin/plugin.json +22 -0
- package/runtime/plugins/agents-init/content.json +31 -0
- package/runtime/plugins/agents-init/plugin.json +19 -0
- package/runtime/plugins/agents-init/skills/agents-init/AGENT.template.md +376 -0
- package/runtime/plugins/agents-init/skills/agents-init/SKILL.md +119 -0
- package/runtime/plugins/agents-init/skills/agents-init/index.html +357 -0
- package/runtime/plugins/agents-init/skills/agents-init/launcher.sh +93 -0
- package/runtime/plugins/agents-init/skills/agents-init/project.schema.json +171 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/init_project.py +1190 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/test_init_project.py +678 -0
- package/runtime/plugins/agents-init/skills/agents-init/skill.json +122 -0
- package/runtime/plugins/opinion-manager/.codex-plugin/plugin.json +26 -0
- package/runtime/plugins/opinion-manager/catalog/README.md +15 -0
- package/runtime/plugins/opinion-manager/catalog/rules/.gitkeep +0 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.align-requirements/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.assumptions-authority/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.batch-questions/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.bilingual-terms/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.calculations/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.chinese-response/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.clean-deliverable/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.complete-options/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.conclusion-first/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.concrete-titles/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.consistent-items/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.device-readable/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.editable-diagrams/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.evidence-inference/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.maintenance-chinese/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.maintenance-project-language/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.markdown-structure/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.portable-files/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.report-context/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.report-formats/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.report-review/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.response-brief/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.response-dense/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.rules-tools/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.semantic-wrap/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.solutions-proportional/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.solutions-three-level/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.tables-compact/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.tables-list-first/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.test-evidence/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/rules/om.versioned-files/versions/1.0.0.json +11 -0
- package/runtime/plugins/opinion-manager/catalog/templates/.gitkeep +0 -0
- package/runtime/plugins/opinion-manager/catalog/templates/delivery-standard/compact-tables/versions/1.0.0.json +21 -0
- package/runtime/plugins/opinion-manager/catalog/templates/delivery-standard/list-first/versions/1.0.0.json +21 -0
- package/runtime/plugins/opinion-manager/catalog/templates/development-standard/chinese-maintenance/versions/1.0.0.json +15 -0
- package/runtime/plugins/opinion-manager/catalog/templates/development-standard/project-language/versions/1.0.0.json +15 -0
- package/runtime/plugins/opinion-manager/catalog/templates/response-style/brief-chinese/versions/1.0.0.json +19 -0
- package/runtime/plugins/opinion-manager/catalog/templates/response-style/dense-chinese/versions/1.0.0.json +19 -0
- package/runtime/plugins/opinion-manager/catalog/templates/scenario/concise-project/versions/1.0.0.json +39 -0
- package/runtime/plugins/opinion-manager/catalog/templates/scenario/reviewed-project/versions/1.0.0.json +39 -0
- package/runtime/plugins/opinion-manager/catalog/templates/work-method/proportional/versions/1.0.0.json +20 -0
- package/runtime/plugins/opinion-manager/catalog/templates/work-method/three-level/versions/1.0.0.json +20 -0
- package/runtime/plugins/opinion-manager/content.json +63 -0
- package/runtime/plugins/opinion-manager/plugin.json +18 -0
- package/runtime/plugins/opinion-manager/references/lock.schema.json +14 -0
- package/runtime/plugins/opinion-manager/references/onboarding.md +57 -0
- package/runtime/plugins/opinion-manager/references/profile.schema.json +56 -0
- package/runtime/plugins/opinion-manager/references/rule.schema.json +16 -0
- package/runtime/plugins/opinion-manager/references/template.schema.json +71 -0
- package/runtime/plugins/opinion-manager/references/versioned-template.schema.json +19 -0
- package/runtime/plugins/opinion-manager/references/versioning.md +263 -0
- package/runtime/plugins/opinion-manager/runtime/constants.py +7 -0
- package/runtime/plugins/opinion-manager/runtime/context.py +180 -0
- package/runtime/plugins/opinion-manager/runtime/lifecycle.py +395 -0
- package/runtime/plugins/opinion-manager/runtime/opinion_manager.py +419 -0
- package/runtime/plugins/opinion-manager/runtime/versioned.py +705 -0
- package/runtime/plugins/opinion-manager/skills/opinion-manager/SKILL.md +62 -0
- package/runtime/plugins/opinion-manager/skills/opinion-manager/agents/openai.yaml +4 -0
- package/runtime/plugins/opinion-manager/templates/.gitkeep +1 -0
- package/runtime/plugins/project-orchestrator/.codex-plugin/plugin.json +18 -0
- package/runtime/plugins/project-orchestrator/content.json +37 -0
- package/runtime/plugins/project-orchestrator/plugin.json +19 -0
- package/runtime/plugins/project-orchestrator/references/asset-synonyms.example.json +8 -0
- package/runtime/plugins/project-orchestrator/references/case-workspaces.md +159 -0
- package/runtime/plugins/project-orchestrator/references/case.schema.json +31 -0
- package/runtime/plugins/project-orchestrator/runtime/assets.py +1288 -0
- package/runtime/plugins/project-orchestrator/runtime/case.py +557 -0
- package/runtime/plugins/project-orchestrator/runtime/case_workspace.py +350 -0
- package/runtime/plugins/project-orchestrator/runtime/memory.py +363 -0
- package/runtime/plugins/project-orchestrator/runtime/project_base.py +107 -0
- package/runtime/plugins/project-orchestrator/runtime/project_cli.py +599 -0
- package/runtime/plugins/project-orchestrator/runtime/project_demo.py +504 -0
- package/runtime/plugins/project-orchestrator/runtime/project_maintain.py +470 -0
- package/runtime/plugins/project-orchestrator/runtime/project_menu.py +394 -0
- package/runtime/plugins/project-orchestrator/runtime/project_real.py +522 -0
- package/runtime/plugins/project-orchestrator/runtime/project_write.py +329 -0
- package/runtime/plugins/project-orchestrator/runtime/wf.py +439 -0
- package/runtime/plugins/project-orchestrator/skills/project-orchestrator/SKILL.md +230 -0
- package/runtime/plugins/project-orchestrator/skills/project-orchestrator/agents/openai.yaml +4 -0
- package/runtime/plugins/project-orchestrator/templates/CASE.md +39 -0
- /package/runtime/{agent_pack_config.py → plugins/agent-pack/runtime/agent_pack_config.py} +0 -0
- /package/runtime/{mcp_env_launcher.py → plugins/agent-pack/runtime/mcp_env_launcher.py} +0 -0
- /package/runtime/{mcp_server.py → plugins/agent-pack/runtime/mcp_server.py} +0 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
"""项目入口的演示模式(--demo):内置几个虚构项目,不读写任何真实文件。
|
|
2
|
+
|
|
3
|
+
用来在没有项目时体验菜单,也是界面测试的固定数据。数据形状与真实来源(project_real)
|
|
4
|
+
一致:资料结果沿用 ``assets.find`` 的字段,事项沿用 ``case.py`` 的 Frontmatter 字段。所有
|
|
5
|
+
“执行”都只返回模拟结果,并带 ``simulated: True``。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import copy
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from project_base import EntryError
|
|
14
|
+
|
|
15
|
+
DEMO_NOW = "2026-10-25T09:12:00+08:00"
|
|
16
|
+
FAULTS = ("index-broken", "offline", "runtime-old", "conflict")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _asset(path: str, title: str, case: str, status: str, modified: str, *, role: str = "deliverable",
|
|
20
|
+
versions: tuple[tuple[str, str], ...] = (), refs: int = 0, git: str = "tracked") -> dict:
|
|
21
|
+
return {"path": path, "name": path.rsplit("/", 1)[-1], "title": title, "role": role,
|
|
22
|
+
"container": case, "case_title": "", "status": status, "modified": modified, "git": git,
|
|
23
|
+
"referenced_by": refs,
|
|
24
|
+
"versions": [{"path": p, "name": p.rsplit("/", 1)[-1], "modified": m, "git": "tracked"}
|
|
25
|
+
for p, m in versions]}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _item(case_id: str, title: str, status: str, updated: str, *, goal: str, next_action: str = "",
|
|
29
|
+
waiting_for: str = "", resume_when: str = "", last: tuple[str, str] | None = None,
|
|
30
|
+
deliverables: tuple[str, ...] = (), aliases: tuple[str, ...] = (), legacy: tuple[str, ...] = (),
|
|
31
|
+
path: str | None = None) -> dict:
|
|
32
|
+
return {"id": case_id, "title": title, "status": status, "goal": goal, "next_action": next_action,
|
|
33
|
+
"waiting_for": waiting_for, "resume_when": resume_when,
|
|
34
|
+
"last_record": {"at": last[0], "text": last[1]} if last else None,
|
|
35
|
+
"deliverables": list(deliverables), "aliases": list(aliases), "legacy_ids": list(legacy),
|
|
36
|
+
"updated_at": updated, "path": path or f"cases/{case_id}/CASE.md"}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
STUDY = {
|
|
40
|
+
"name": "日语 N2 备考", "kind": "学习",
|
|
41
|
+
"about": "按周安排语法、词汇、听力与阅读,把笔记、练习记录和模拟题放在一起,随时看到复习到哪一步。",
|
|
42
|
+
"launcher": "menu",
|
|
43
|
+
"items_source": "cases/*/CASE.md(默认 Case 工具)",
|
|
44
|
+
"items": [
|
|
45
|
+
_item("CASE-20261003-002-grammar-review", "语法总复习", "active", "2026-10-24T21:30:00+08:00",
|
|
46
|
+
goal="把 N2 语法 1–20 课过完一遍并整理错题", next_action="完成第 12–15 课练习",
|
|
47
|
+
last=("2026-10-24T21:30:00+08:00", "完成第 11 课错题整理,新增 6 条易混语法"),
|
|
48
|
+
deliverables=("cases/CASE-20261003-002-grammar-review/grammar-summary-v3.md",),
|
|
49
|
+
aliases=("语法",)),
|
|
50
|
+
_item("CASE-20261005-001-listening-daily", "听力每日练习", "active", "2026-10-23T22:05:00+08:00",
|
|
51
|
+
goal="每天 20 分钟精听并记录正确率", next_action="今天听第 18 套第 2 部分",
|
|
52
|
+
last=("2026-10-23T22:05:00+08:00", "第 17 套正确率 72%"),
|
|
53
|
+
deliverables=("cases/CASE-20261005-001-listening-daily/listening-log.csv",)),
|
|
54
|
+
_item("CASE-20261010-003-exam-signup", "报名 12 月考试", "waiting", "2026-10-20T12:00:00+08:00",
|
|
55
|
+
goal="完成 12 月考试报名并保存准考信息", waiting_for="报名网站开放",
|
|
56
|
+
resume_when="官网公布报名时间后(预计 10-28)",
|
|
57
|
+
last=("2026-10-20T12:00:00+08:00", "确认报名所需材料:照片、证件扫描件")),
|
|
58
|
+
_item("CASE-20260915-002-reading", "长文精读", "suspended", "2026-10-02T20:00:00+08:00",
|
|
59
|
+
goal="每周精读两篇长文", next_action="恢复后从第 6 篇开始",
|
|
60
|
+
resume_when="语法总复习完成后",
|
|
61
|
+
last=("2026-10-02T20:00:00+08:00", "暂停:先集中复习语法")),
|
|
62
|
+
_item("CASE-20260901-001-vocab-round1", "词汇第一轮", "done", "2026-09-28T21:00:00+08:00",
|
|
63
|
+
goal="背完核心词汇 2000 个", last=("2026-09-28T21:00:00+08:00", "第一轮完成,导出卡片"),
|
|
64
|
+
deliverables=("cases/CASE-20260901-001-vocab-round1/vocab-cards.apkg",),
|
|
65
|
+
aliases=("单词", "词汇"), legacy=("T-0007",)),
|
|
66
|
+
],
|
|
67
|
+
"assets": [
|
|
68
|
+
_asset("cases/CASE-20261003-002-grammar-review/grammar-summary-v3.md", "语法总结(第 3 版)",
|
|
69
|
+
"CASE-20261003-002-grammar-review", "active", "2026-10-24T21:28:00+08:00", refs=2,
|
|
70
|
+
versions=(("cases/CASE-20261003-002-grammar-review/grammar-summary-v2.md", "2026-10-15T20:10:00+08:00"),
|
|
71
|
+
("cases/CASE-20261003-002-grammar-review/grammar-summary-v1.md", "2026-10-06T19:40:00+08:00"))),
|
|
72
|
+
_asset("cases/CASE-20261005-001-listening-daily/listening-log.csv", "听力练习记录",
|
|
73
|
+
"CASE-20261005-001-listening-daily", "active", "2026-10-23T22:05:00+08:00", role="data", refs=1),
|
|
74
|
+
_asset("materials/mock-exams/2025-12-mock.pdf", "模拟题 2025 年 12 月", "materials", "",
|
|
75
|
+
"2026-09-20T10:00:00+08:00", role="source", refs=1),
|
|
76
|
+
_asset("materials/mock-exams/2025-07-mock.pdf", "模拟题 2025 年 7 月", "materials", "",
|
|
77
|
+
"2026-09-20T10:00:00+08:00", role="source"),
|
|
78
|
+
_asset("cases/CASE-20260901-001-vocab-round1/vocab-cards.apkg", "词汇卡片导出",
|
|
79
|
+
"CASE-20260901-001-vocab-round1", "done", "2026-09-28T21:00:00+08:00", refs=1),
|
|
80
|
+
_asset("study-plan.md", "学习计划", "project", "", "2026-10-01T09:00:00+08:00",
|
|
81
|
+
role="project-file", refs=0),
|
|
82
|
+
],
|
|
83
|
+
"refs": {
|
|
84
|
+
"cases/CASE-20261003-002-grammar-review/grammar-summary-v3.md": {
|
|
85
|
+
"inbound": ["cases/CASE-20261003-002-grammar-review/CASE.md", "study-plan.md"],
|
|
86
|
+
"outbound": ["materials/mock-exams/2025-12-mock.pdf", "img/particles.png(不存在)"]},
|
|
87
|
+
},
|
|
88
|
+
"file_count": 46,
|
|
89
|
+
"where": [("事项与各自资料", "cases/"), ("教材与模拟题", "materials/"), ("学习计划", "study-plan.md")],
|
|
90
|
+
"start": ["先看「看进展」里进行中的事项和下一动作", "需要哪份笔记或模拟题,用「找资料」输入关键词",
|
|
91
|
+
"做完一步后,用「记录事项」追加一条记录"],
|
|
92
|
+
"examples": [("找去年 12 月的模拟题", "1 → 模拟题 2025-12", "find 模拟题 2025-12"),
|
|
93
|
+
("看语法复习到哪了", "2 → 输入 语法", "show 语法"),
|
|
94
|
+
("按旧编号找已完成的词汇事项", "2 → 输入 T-0007", "show T-0007")],
|
|
95
|
+
"health": [
|
|
96
|
+
{"id": "broken-link", "level": "suggest", "area": "资料", "title": "资料里有 1 处断链",
|
|
97
|
+
"reason": "grammar-summary-v3.md 引用的 img/particles.png 不存在",
|
|
98
|
+
"impact": "打开这份笔记时缺一张图;不影响其他资料",
|
|
99
|
+
"next": [{"label": "打开笔记修正链接", "action": "open:cases/CASE-20261003-002-grammar-review/grammar-summary-v3.md"}]},
|
|
100
|
+
{"id": "update-orchestrator", "level": "update", "area": "插件", "title": "project-orchestrator 有新版本",
|
|
101
|
+
"reason": "已装 1.7.0,云端 1.8.0(只改代码,无不兼容变更)", "impact": "不更新也能继续使用",
|
|
102
|
+
"next": [{"label": "查看更新内容并更新", "action": "fix:update-orchestrator"}]},
|
|
103
|
+
{"id": "opinion-missing", "level": "unavailable", "area": "规则", "title": "行为规则(Opinion)未配置",
|
|
104
|
+
"reason": "项目没有 OPINION.md 的已确认版本", "impact": "只影响 AI Agent 的行为约定;本菜单不受影响",
|
|
105
|
+
"next": [{"label": "需要时在 Agent 会话中用 opinion-manager 配置", "action": ""}]},
|
|
106
|
+
],
|
|
107
|
+
"ok": ["运行环境(Python 3.12)", "插件兼容", "事项记录格式", "资料索引"],
|
|
108
|
+
"fixes": {
|
|
109
|
+
"update-orchestrator": {
|
|
110
|
+
"title": "更新 project-orchestrator 1.7.0 → 1.8.0", "kind": "plugin", "network": True,
|
|
111
|
+
"changes": ["新增:看进展支持按标签筛选", "修复:日期查找对“10月”识别不全"],
|
|
112
|
+
"writes": [{"path": "~/.claude/plugins/cache/agenthelm/project-orchestrator/1.8.0/", "scope": "客户端插件目录"}],
|
|
113
|
+
"custom": [], "breaking": False,
|
|
114
|
+
"recovery": "旧版本仍保留在客户端缓存,可用 agenthelm update 回到 1.7.0",
|
|
115
|
+
"verify": "重新读取已安装版本:1.8.0"},
|
|
116
|
+
},
|
|
117
|
+
"record": {"provider": "default", "write": True},
|
|
118
|
+
"tools": [],
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
PERSONAL = {
|
|
122
|
+
"name": "家里搬家", "kind": "个人事务",
|
|
123
|
+
"about": "整理找房、比价、宽带迁移和旧物处理,把合同扫描件、报价和清单放在各自事项里。",
|
|
124
|
+
"launcher": "menu",
|
|
125
|
+
"items_source": "cases/*/CASE.md(默认 Case 工具)",
|
|
126
|
+
"items": [
|
|
127
|
+
_item("CASE-20261008-001-movers-quote", "搬家公司比价", "waiting", "2026-10-22T19:00:00+08:00",
|
|
128
|
+
goal="选定搬家公司并确定日期", waiting_for="B 公司回复报价",
|
|
129
|
+
resume_when="收到报价后对比三家", last=("2026-10-22T19:00:00+08:00", "A、C 两家报价已收到,已填入对比表"),
|
|
130
|
+
deliverables=("cases/CASE-20261008-001-movers-quote/quote-compare-v2.xlsx",), aliases=("搬家公司",)),
|
|
131
|
+
_item("CASE-20261009-002-broadband", "宽带迁移", "active", "2026-10-24T10:30:00+08:00",
|
|
132
|
+
goal="新家入住当天能上网", next_action="打电话预约 11-02 上门",
|
|
133
|
+
last=("2026-10-24T10:30:00+08:00", "确认新地址可办理")),
|
|
134
|
+
_item("CASE-20261011-001-old-furniture", "旧家具处理", "active", "2026-10-21T21:00:00+08:00",
|
|
135
|
+
goal="搬家前处理掉不带走的家具", next_action="给书柜拍照挂到二手平台",
|
|
136
|
+
last=("2026-10-21T21:00:00+08:00", "列出 7 件不带走的家具"),
|
|
137
|
+
deliverables=("cases/CASE-20261011-001-old-furniture/furniture-list.md",)),
|
|
138
|
+
_item("CASE-20261020-001-address-change", "地址变更清单", "captured", "2026-10-20T08:00:00+08:00",
|
|
139
|
+
goal="(待澄清)哪些地方需要改地址"),
|
|
140
|
+
_item("CASE-20260920-001-find-flat", "找房", "done", "2026-10-05T18:00:00+08:00",
|
|
141
|
+
goal="签下新房", last=("2026-10-05T18:00:00+08:00", "已签约,扫描件已存档"),
|
|
142
|
+
deliverables=("cases/CASE-20260920-001-find-flat/lease-scan.pdf",), legacy=("找房子",)),
|
|
143
|
+
],
|
|
144
|
+
"assets": [
|
|
145
|
+
_asset("cases/CASE-20261008-001-movers-quote/quote-compare-v2.xlsx", "搬家报价对比(第 2 版)",
|
|
146
|
+
"CASE-20261008-001-movers-quote", "waiting", "2026-10-22T19:00:00+08:00", refs=1,
|
|
147
|
+
versions=(("cases/CASE-20261008-001-movers-quote/quote-compare-v1.xlsx", "2026-10-15T21:00:00+08:00"),)),
|
|
148
|
+
_asset("cases/CASE-20260920-001-find-flat/lease-scan.pdf", "租房合同扫描件",
|
|
149
|
+
"CASE-20260920-001-find-flat", "done", "2026-10-05T18:00:00+08:00", refs=1, git="ignored"),
|
|
150
|
+
_asset("cases/CASE-20261011-001-old-furniture/furniture-list.md", "不带走的家具清单",
|
|
151
|
+
"CASE-20261011-001-old-furniture", "active", "2026-10-21T21:00:00+08:00", refs=1),
|
|
152
|
+
_asset("cases/CASE-20261011-001-old-furniture/photos/bookshelf.jpg", "书柜照片",
|
|
153
|
+
"CASE-20261011-001-old-furniture", "active", "2026-10-21T21:05:00+08:00", role="image"),
|
|
154
|
+
],
|
|
155
|
+
"refs": {},
|
|
156
|
+
"file_count": 19,
|
|
157
|
+
"where": [("事项与各自资料", "cases/"), ("合同等扫描件", "各事项目录内(不入 Git)")],
|
|
158
|
+
"start": ["「看进展」先看等待中的事项:等谁、什么时候恢复", "找报价或合同,用「找资料」",
|
|
159
|
+
"电话打完、事情办完,用「记录事项」追加一条"],
|
|
160
|
+
"examples": [("找租房合同", "1 → 合同", "find 合同"), ("看还在等谁", "2 → 看「等待」分组", "progress --status waiting")],
|
|
161
|
+
"health": [
|
|
162
|
+
{"id": "template-update", "level": "update", "area": "项目模板", "title": "项目说明模板有新版本",
|
|
163
|
+
"reason": "agents-init 项目契约模板 1.0.0 → 1.1.0;其中 AGENTS.md 你改过", "impact": "不更新不影响使用;更新时你改过的文件逐个选择保留或采用",
|
|
164
|
+
"next": [{"label": "查看影响并选择", "action": "fix:template-update"}]},
|
|
165
|
+
{"id": "no-ai-client", "level": "unavailable", "area": "环境", "title": "本机没有安装 AI 客户端",
|
|
166
|
+
"reason": "没有找到 Claude Code、Codex 或 Cursor", "impact": "本菜单的全部日常功能不受影响;只有“安装插件到客户端”不可用",
|
|
167
|
+
"next": [{"label": "需要时安装任一客户端后,在 5 修复与更新 里安装插件", "action": ""}]},
|
|
168
|
+
],
|
|
169
|
+
"ok": ["运行环境(Python 3.11)", "事项记录格式", "资料索引"],
|
|
170
|
+
"fixes": {
|
|
171
|
+
"template-update": {
|
|
172
|
+
"title": "项目说明模板 1.0.0 → 1.1.0", "kind": "template", "network": False,
|
|
173
|
+
"changes": ["AGENTS.md:新增「人用入口」一节", "新增启动器说明"],
|
|
174
|
+
"writes": [{"path": "AGENTS.md", "scope": "项目"}, {"path": "CLAUDE.md", "scope": "项目"},
|
|
175
|
+
{"path": ".agents/moe.sakanano.agent-pack/content.lock.json", "scope": "项目"}],
|
|
176
|
+
"custom": [{"path": "AGENTS.md", "state": "你改过", "default": "keep"}],
|
|
177
|
+
"untouched": ["CLAUDE.md"], "breaking": False,
|
|
178
|
+
"recovery": "~/.agent-pack/recovery/20261025-091500-content/",
|
|
179
|
+
"verify": "逐个比对写入后的摘要与预览一致;保留的文件内容不变"},
|
|
180
|
+
},
|
|
181
|
+
"record": {"provider": "default", "write": True},
|
|
182
|
+
"tools": [],
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
RESEARCH = {
|
|
186
|
+
"name": "城市鸟类观察", "kind": "研究",
|
|
187
|
+
"about": "按观察点记录鸟类,整理照片与观察表,定期汇总成季度报告。事项由项目自己的工具 tools/obs.py 管理。",
|
|
188
|
+
"launcher": "obs",
|
|
189
|
+
"items_source": "项目工具 tools/obs.py(只读接口 list/show)",
|
|
190
|
+
"status_map": {"进行中": "active", "待复核": "waiting", "归档": "done"},
|
|
191
|
+
"items": [
|
|
192
|
+
_item("OBS-2026Q4-01", "公园东湖越冬观察", "进行中", "2026-10-24T07:40:00+08:00",
|
|
193
|
+
goal="每周两次记录东湖水鸟种类与数量", next_action="周日早上第 3 次观察",
|
|
194
|
+
last=("2026-10-24T07:40:00+08:00", "记录 14 种,新增 1 种待确认"),
|
|
195
|
+
deliverables=("data/east-lake/2026-10-24.csv",), path="observations/OBS-2026Q4-01.yaml"),
|
|
196
|
+
_item("OBS-2026Q4-02", "待确认物种:疑似黑尾塍鹬", "待复核", "2026-10-24T08:10:00+08:00",
|
|
197
|
+
goal="确认 10-24 拍到的个体", waiting_for="观鸟群里有经验的人回复",
|
|
198
|
+
last=("2026-10-24T08:10:00+08:00", "照片已发出"), deliverables=("photos/2026-10-24/IMG_0412.jpg",),
|
|
199
|
+
path="observations/OBS-2026Q4-02.yaml"),
|
|
200
|
+
_item("OBS-2026Q3-05", "第三季度汇总", "归档", "2026-10-03T20:00:00+08:00",
|
|
201
|
+
goal="汇总 7–9 月观察数据", last=("2026-10-03T20:00:00+08:00", "报告完成"),
|
|
202
|
+
deliverables=("reports/2026-Q3-summary.md",), legacy=("Q3汇总",),
|
|
203
|
+
path="observations/OBS-2026Q3-05.yaml"),
|
|
204
|
+
],
|
|
205
|
+
"assets": [
|
|
206
|
+
_asset("reports/2026-Q3-summary.md", "第三季度观察汇总", "reports", "", "2026-10-03T20:00:00+08:00", refs=1,
|
|
207
|
+
versions=(("reports/2026-Q3-summary-draft.md", "2026-10-01T22:00:00+08:00"),)),
|
|
208
|
+
_asset("data/east-lake/2026-10-24.csv", "东湖观察表 10-24", "data", "", "2026-10-24T07:40:00+08:00",
|
|
209
|
+
role="data", refs=1),
|
|
210
|
+
_asset("photos/2026-10-24/IMG_0412.jpg", "疑似黑尾塍鹬", "photos", "", "2026-10-24T07:20:00+08:00",
|
|
211
|
+
role="image", refs=1, git="ignored"),
|
|
212
|
+
],
|
|
213
|
+
"refs": {},
|
|
214
|
+
"file_count": 312,
|
|
215
|
+
"where": [("观察记录(权威)", "observations/"), ("观察表", "data/"), ("照片(不入 Git)", "photos/"),
|
|
216
|
+
("季度报告", "reports/")],
|
|
217
|
+
"start": ["用 ./obs 打开本菜单", "「看进展」显示 tools/obs.py 提供的观察事项",
|
|
218
|
+
"新建或修改观察记录请用 tools/obs.py 本身(本入口暂不支持写入,原因见「记录事项」)"],
|
|
219
|
+
"examples": [("找第三季度报告", "1 → 季度 汇总", "find 季度 汇总"),
|
|
220
|
+
("导出观察数据", "7 → 导出观察表", "tools export-csv")],
|
|
221
|
+
"health": [
|
|
222
|
+
{"id": "write-gap", "level": "unavailable", "area": "事项", "title": "记录事项不可用",
|
|
223
|
+
"reason": "项目工具 tools/obs.py 只提供 list/show,没有写接口", "impact": "可以浏览与查找;新建和修改请直接用 tools/obs.py",
|
|
224
|
+
"next": [{"label": "如需在菜单里写入,请项目维护者为 obs.py 增加 append / set-status 接口", "action": ""}]},
|
|
225
|
+
{"id": "sync-unavailable", "level": "unavailable", "area": "项目工具", "title": "“同步到在线物种库”当前不可用",
|
|
226
|
+
"reason": "缺少环境变量 BIRDDB_TOKEN(账号令牌)", "impact": "只影响这一项工具",
|
|
227
|
+
"next": [{"label": "在本机设置 BIRDDB_TOKEN 后重新检查", "action": ""}]},
|
|
228
|
+
],
|
|
229
|
+
"ok": ["运行环境(Python 3.13)", "项目工具 tools/obs.py 可运行", "资料索引"],
|
|
230
|
+
"fixes": {},
|
|
231
|
+
"record": {"provider": "project-tool", "write": False,
|
|
232
|
+
"gap": "项目声明了自有工具 tools/obs.py,它只有只读接口(list / show),没有写接口。"
|
|
233
|
+
"本入口不会绕过项目工具直接改写 observations/ 下的文件。"},
|
|
234
|
+
"tools": [
|
|
235
|
+
{"id": "export-csv", "title": "导出观察表", "description": "把全部观察记录导出为一个 CSV",
|
|
236
|
+
"command": ["python3", "tools/obs.py", "export", "--format", "csv", "--to", "exports/"],
|
|
237
|
+
"network": False, "account": None, "writes": ["exports/"], "requires": ["python3"],
|
|
238
|
+
"state": "available", "reason": ""},
|
|
239
|
+
{"id": "sync-db", "title": "同步到在线物种库", "description": "把本季度记录上传到在线物种库",
|
|
240
|
+
"command": ["python3", "tools/obs.py", "sync"], "network": True, "account": "BIRDDB_TOKEN",
|
|
241
|
+
"writes": ["在线物种库(外部系统)"], "requires": ["python3", "网络", "BIRDDB_TOKEN"],
|
|
242
|
+
"state": "unavailable", "reason": "缺少环境变量 BIRDDB_TOKEN"},
|
|
243
|
+
],
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
BLANK = {
|
|
247
|
+
"name": "新项目", "kind": "通用",
|
|
248
|
+
"about": "刚初始化的项目,还没有事项和资料。",
|
|
249
|
+
"launcher": "menu",
|
|
250
|
+
"items_source": "cases/*/CASE.md(默认 Case 工具)",
|
|
251
|
+
"items": [], "assets": [], "refs": {}, "file_count": 0,
|
|
252
|
+
"where": [("事项与各自资料", "cases/(目前为空)"), ("参考资料", "docs/refs/")],
|
|
253
|
+
"start": ["用「记录事项」新建第一个事项", "把资料放进事项目录或 docs/refs/,之后就能在「找资料」里查到"],
|
|
254
|
+
"examples": [("新建第一个事项", "6 → n", "record new --title 第一个事项 --goal 写下目标 --apply")],
|
|
255
|
+
"health": [], "ok": ["运行环境", "插件兼容", "事项记录格式(暂无事项)"], "fixes": {},
|
|
256
|
+
"record": {"provider": "default", "write": True}, "tools": [],
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
SCENARIOS = {"study": STUDY, "personal": PERSONAL, "research": RESEARCH, "blank": BLANK}
|
|
260
|
+
DEFAULT_GROUPS = {"active": "active", "waiting": "waiting", "captured": "captured", "suspended": "suspended",
|
|
261
|
+
"done": "done", "abandoned": "abandoned"}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
DemoError = EntryError
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class DemoProvider:
|
|
268
|
+
"""内存里的虚构项目;所有结果带 demo 标记,所有写入只模拟。"""
|
|
269
|
+
|
|
270
|
+
simulated = True
|
|
271
|
+
|
|
272
|
+
def __init__(self, scenario: str, fault: str | None = None) -> None:
|
|
273
|
+
if scenario not in SCENARIOS:
|
|
274
|
+
raise DemoError(f"没有这个演示场景:{scenario}", "usage", [f"可选:{'、'.join(SCENARIOS)}"])
|
|
275
|
+
if fault is not None and fault not in FAULTS:
|
|
276
|
+
raise DemoError(f"没有这个演示故障:{fault}", "usage", [f"可选:{'、'.join(FAULTS)}"])
|
|
277
|
+
self.scenario_id = scenario
|
|
278
|
+
self.data = copy.deepcopy(SCENARIOS[scenario])
|
|
279
|
+
self.fault = fault
|
|
280
|
+
titles = {item["id"]: item["title"] for item in self.data["items"]}
|
|
281
|
+
for asset in self.data["assets"]:
|
|
282
|
+
asset["case_title"] = titles.get(asset["container"], "")
|
|
283
|
+
self.name = self.data["name"]
|
|
284
|
+
self.entry_api = 2 if fault == "runtime-old" else 1
|
|
285
|
+
self.resolved: set[str] = set() # fixes simulated in this session
|
|
286
|
+
|
|
287
|
+
# -------------------------------------------------------------- overview
|
|
288
|
+
|
|
289
|
+
def index_state(self) -> dict:
|
|
290
|
+
if self.fault == "index-broken" and "rebuild-index" not in self.resolved:
|
|
291
|
+
return {"status": "broken", "built_at": "2026-10-25T08:50:00+08:00", "file_count": 0}
|
|
292
|
+
return {"status": "fresh", "built_at": DEMO_NOW, "file_count": self.data["file_count"]}
|
|
293
|
+
|
|
294
|
+
def _group(self, item: dict) -> str:
|
|
295
|
+
mapping = self.data.get("status_map") or DEFAULT_GROUPS
|
|
296
|
+
return mapping.get(item["status"], "other")
|
|
297
|
+
|
|
298
|
+
def overview(self) -> dict:
|
|
299
|
+
counts: dict[str, int] = {}
|
|
300
|
+
for item in self.data["items"]:
|
|
301
|
+
counts[self._group(item)] = counts.get(self._group(item), 0) + 1
|
|
302
|
+
levels: dict[str, int] = {}
|
|
303
|
+
for finding in self.health()["findings"]:
|
|
304
|
+
levels[finding["level"]] = levels.get(finding["level"], 0) + 1
|
|
305
|
+
return {"name": self.data["name"], "kind": self.data["kind"], "kind_label": self.data["kind"] + "项目",
|
|
306
|
+
"launcher": self.data["launcher"],
|
|
307
|
+
"index": self.index_state(), "items": counts, "health": levels, "read_at": DEMO_NOW,
|
|
308
|
+
"tools": {"declared": len(self.data["tools"]),
|
|
309
|
+
"available": sum(tool["state"] == "available" for tool in self.data["tools"])}}
|
|
310
|
+
|
|
311
|
+
# -------------------------------------------------------------- find
|
|
312
|
+
|
|
313
|
+
def find(self, terms: list[str], *, ext: str | None = None, container: str | None = None,
|
|
314
|
+
limit: int = 10) -> dict:
|
|
315
|
+
if self.index_state()["status"] == "broken":
|
|
316
|
+
raise DemoError("资料索引损坏,暂时无法查找", "index-broken",
|
|
317
|
+
["重建索引:只改用户缓存里的索引,不改项目文件(5 修复与更新 → 重建索引,或 fix rebuild-index --apply)"])
|
|
318
|
+
results = []
|
|
319
|
+
for asset in self.data["assets"]:
|
|
320
|
+
haystack = " ".join([asset["path"], asset["title"], asset["case_title"], asset["modified"]]).lower()
|
|
321
|
+
if ext and not asset["name"].lower().endswith("." + ext.lower().lstrip(".")):
|
|
322
|
+
continue
|
|
323
|
+
if container and container.lower() not in asset["container"].lower():
|
|
324
|
+
continue
|
|
325
|
+
if all(_term_hits(term, haystack) for term in terms):
|
|
326
|
+
results.append(asset)
|
|
327
|
+
results.sort(key=lambda asset: asset["modified"], reverse=True)
|
|
328
|
+
return {"ok": True, "total": len(results), "returned": min(len(results), limit),
|
|
329
|
+
"results": results[:limit], "query": {"terms": terms}, "index": self.index_state()}
|
|
330
|
+
|
|
331
|
+
def refs(self, path: str) -> dict:
|
|
332
|
+
known = self.data["refs"].get(path, {})
|
|
333
|
+
return {"path": path, "inbound": known.get("inbound", []), "outbound": known.get("outbound", [])}
|
|
334
|
+
|
|
335
|
+
def act(self, action: str, path: str) -> dict:
|
|
336
|
+
verbs = {"open": "打开", "reveal": "在文件管理器中显示", "copy": "复制路径"}
|
|
337
|
+
return {"action": action, "path": path, "simulated": True,
|
|
338
|
+
"message": f"【演示】将{verbs.get(action, action)} {path}(未实际执行)"}
|
|
339
|
+
|
|
340
|
+
# -------------------------------------------------------------- progress
|
|
341
|
+
|
|
342
|
+
def items(self, *, status: str | None = None, term: str | None = None) -> dict:
|
|
343
|
+
rows = []
|
|
344
|
+
for item in self.data["items"]:
|
|
345
|
+
group = self._group(item)
|
|
346
|
+
if status and group != status:
|
|
347
|
+
continue
|
|
348
|
+
if term and not _item_hits(term, item):
|
|
349
|
+
continue
|
|
350
|
+
rows.append({**item, "group": group})
|
|
351
|
+
return {"items": rows, "source": self.data["items_source"], "read_at": DEMO_NOW}
|
|
352
|
+
|
|
353
|
+
def item(self, token: str) -> dict:
|
|
354
|
+
exact = [item for item in self.data["items"] if token in (item["id"], *item["legacy_ids"], *item["aliases"])]
|
|
355
|
+
matches = exact or [item for item in self.data["items"] if _item_hits(token, item)]
|
|
356
|
+
if not matches:
|
|
357
|
+
raise DemoError(f"没有找到事项:{token}", "not-found", ["试试更短的词,或在「看进展」里浏览全部"])
|
|
358
|
+
if len(matches) > 1:
|
|
359
|
+
raise DemoError(f"有 {len(matches)} 个事项都匹配“{token}”", "ambiguous",
|
|
360
|
+
[f"{item['id']} {item['title']}" for item in matches])
|
|
361
|
+
item = matches[0]
|
|
362
|
+
return {**item, "group": self._group(item), "source": self.data["items_source"]}
|
|
363
|
+
|
|
364
|
+
# -------------------------------------------------------------- guide / health
|
|
365
|
+
|
|
366
|
+
def guide(self) -> dict:
|
|
367
|
+
return {"name": self.data["name"], "kind": self.data["kind"], "about": self.data["about"],
|
|
368
|
+
"launcher": self.data["launcher"], "where": self.data["where"], "start": self.data["start"],
|
|
369
|
+
"examples": self.data["examples"], "capabilities": self.capabilities()}
|
|
370
|
+
|
|
371
|
+
def capabilities(self) -> list[dict]:
|
|
372
|
+
index = self.index_state()["status"]
|
|
373
|
+
record = self.data["record"]
|
|
374
|
+
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": "项目工具",
|
|
386
|
+
"state": "available" if self.data["tools"] else "unavailable",
|
|
387
|
+
"reason": "" if self.data["tools"] else "项目没有声明专用工具", "offline": True},
|
|
388
|
+
]
|
|
389
|
+
|
|
390
|
+
def health(self) -> dict:
|
|
391
|
+
findings = [finding for finding in copy.deepcopy(self.data["health"])
|
|
392
|
+
if not any(step["action"] == f"fix:{fix_id}" for step in finding["next"] for fix_id in self.resolved)]
|
|
393
|
+
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"}]})
|
|
398
|
+
online = {"status": "offline", "cached_at": "2026-10-22T09:00:00+08:00"} if self.fault == "offline" \
|
|
399
|
+
else {"status": "online", "cached_at": DEMO_NOW}
|
|
400
|
+
return {"findings": findings, "ok": self.data["ok"], "read_at": DEMO_NOW, "update_source": online}
|
|
401
|
+
|
|
402
|
+
# -------------------------------------------------------------- fix
|
|
403
|
+
|
|
404
|
+
def fix_plan(self, ids: list[str], choices: dict[str, str] | None = None) -> dict:
|
|
405
|
+
fixes = {key: value for key, value in self.data["fixes"].items() if key not in self.resolved}
|
|
406
|
+
if self.index_state()["status"] == "broken":
|
|
407
|
+
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": "重新读取索引状态:最新,文件数与项目一致"}
|
|
413
|
+
chosen = ids or sorted(fixes)
|
|
414
|
+
unknown = [fix_id for fix_id in chosen if fix_id not in fixes]
|
|
415
|
+
if unknown:
|
|
416
|
+
raise DemoError("没有这些可处理项:" + "、".join(unknown), "not-found",
|
|
417
|
+
["可处理:" + ("、".join(sorted(fixes)) or "(无)")])
|
|
418
|
+
steps = []
|
|
419
|
+
for fix_id in chosen:
|
|
420
|
+
step = {"id": fix_id, **copy.deepcopy(fixes[fix_id])}
|
|
421
|
+
step["blocked"] = "离线:需要联网,只能预览" if step["network"] and self.fault == "offline" else ""
|
|
422
|
+
steps.append(step)
|
|
423
|
+
return {"steps": steps, "simulated": True, "update_source": self.health()["update_source"]}
|
|
424
|
+
|
|
425
|
+
def apply_fix(self, plan: dict, choices: dict[str, str]) -> dict:
|
|
426
|
+
results = []
|
|
427
|
+
for step in plan["steps"]:
|
|
428
|
+
if step["blocked"]:
|
|
429
|
+
results.append({"id": step["id"], "status": "skipped", "detail": step["blocked"]})
|
|
430
|
+
continue
|
|
431
|
+
kept = [item["path"] for item in step["custom"] if choices.get(item["path"], item["default"]) == "keep"]
|
|
432
|
+
accepted = [item["path"] for item in step["custom"] if item["path"] not in kept]
|
|
433
|
+
written = [w["path"] for w in step["writes"] if w["path"] not in kept]
|
|
434
|
+
self.resolved.add(step["id"])
|
|
435
|
+
results.append({"id": step["id"], "status": "simulated", "written": written, "kept": kept,
|
|
436
|
+
"accepted": accepted, "recovery": step["recovery"], "verify": step["verify"]})
|
|
437
|
+
return {"results": results, "simulated": True}
|
|
438
|
+
|
|
439
|
+
# -------------------------------------------------------------- record
|
|
440
|
+
|
|
441
|
+
def record_preview(self, target: str | None, change: dict) -> dict:
|
|
442
|
+
record = self.data["record"]
|
|
443
|
+
if not record["write"]:
|
|
444
|
+
raise DemoError("这个项目不能在本入口记录事项", "unavailable",
|
|
445
|
+
[record["gap"], "需要时请直接使用项目工具,或请项目维护者补充写接口"])
|
|
446
|
+
if target is None:
|
|
447
|
+
slug = re.sub(r"[^a-z0-9]+", "-", (change.get("slug") or "new-item").lower()).strip("-") or "new-item"
|
|
448
|
+
path = f"cases/CASE-20261025-001-{slug}/CASE.md"
|
|
449
|
+
lines = [f"+ 新建 {path}", f"+ title: {change.get('title', '')}", "+ status: active",
|
|
450
|
+
f"+ goal: {change.get('goal', '')}", f"+ next_action: {change.get('next_action', '')}"]
|
|
451
|
+
return {"target": None, "path": path, "lines": lines, "expect_updated_at": None, "simulated": True}
|
|
452
|
+
item = self.item(target)
|
|
453
|
+
lines = []
|
|
454
|
+
for key, label in (("status", "status"), ("next_action", "next_action"), ("waiting_for", "waiting_for")):
|
|
455
|
+
if change.get(key) is not None:
|
|
456
|
+
lines += [f"- {label}: {item.get(key) or ''}", f"+ {label}: {change[key]}"]
|
|
457
|
+
if change.get("log"):
|
|
458
|
+
lines.append(f"+ 工作记录:- 2026-10-25T09:15 {change['log']}")
|
|
459
|
+
for path in change.get("add_deliverable") or []:
|
|
460
|
+
lines.append(f"+ deliverables: {path}")
|
|
461
|
+
if not lines:
|
|
462
|
+
raise DemoError("没有要修改的内容", "usage", ["至少给出一项:记录、下一动作、等待对象或状态"])
|
|
463
|
+
lines.append(f" updated_at: {item['updated_at']} → 2026-10-25T09:15:00+08:00")
|
|
464
|
+
return {"target": item["id"], "path": item["path"], "lines": lines,
|
|
465
|
+
"expect_updated_at": item["updated_at"], "simulated": True}
|
|
466
|
+
|
|
467
|
+
def record_apply(self, preview: dict) -> dict:
|
|
468
|
+
if self.fault == "conflict" and preview["target"]:
|
|
469
|
+
raise DemoError("这个事项在你预览之后被修改过,没有写入", "conflict",
|
|
470
|
+
["最新修改:2026-10-25T09:14(CASE.md 的 updated_at 已变化)",
|
|
471
|
+
"重新打开预览,确认对方的改动后再提交"])
|
|
472
|
+
return {"path": preview["path"], "simulated": True,
|
|
473
|
+
"verify": "【演示】写入后重新读取:内容与预览一致,未知字段与原有正文保留"}
|
|
474
|
+
|
|
475
|
+
# -------------------------------------------------------------- tools
|
|
476
|
+
|
|
477
|
+
def tools(self) -> list[dict]:
|
|
478
|
+
return copy.deepcopy(self.data["tools"])
|
|
479
|
+
|
|
480
|
+
def run_tool(self, tool_id: str) -> dict:
|
|
481
|
+
tool = next((tool for tool in self.data["tools"] if tool["id"] == tool_id), None)
|
|
482
|
+
if tool is None:
|
|
483
|
+
raise DemoError(f"项目没有声明这个工具:{tool_id}", "not-found")
|
|
484
|
+
if tool["state"] != "available":
|
|
485
|
+
raise DemoError(f"“{tool['title']}”当前不可用", "unavailable", [tool["reason"]])
|
|
486
|
+
return {"id": tool_id, "simulated": True,
|
|
487
|
+
"message": f"【演示】将运行:{' '.join(tool['command'])}(未实际执行)"}
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _term_hits(term: str, haystack: str) -> bool:
|
|
491
|
+
lowered = term.lower()
|
|
492
|
+
month = re.fullmatch(r"(\d{1,2})月", lowered)
|
|
493
|
+
if month:
|
|
494
|
+
return f"-{int(month.group(1)):02d}" in haystack
|
|
495
|
+
return lowered in haystack
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _item_hits(term: str, item: dict) -> bool:
|
|
499
|
+
fields = [item["id"], item["title"], item["goal"], *item["aliases"], *item["legacy_ids"]]
|
|
500
|
+
return term.lower() in " ".join(fields).lower()
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def describe() -> list[dict[str, Any]]:
|
|
504
|
+
return [{"id": key, "name": value["name"], "kind": value["kind"]} for key, value in SCENARIOS.items()]
|