@my-life-buddies/cli 0.14.1 → 0.15.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.
Files changed (188) hide show
  1. package/README.md +63 -15
  2. package/dist/bin/buddy.js +28 -9
  3. package/dist/bin/buddy.js.map +2 -2
  4. package/dist/bin/core.js +1293 -603
  5. package/dist/bin/core.js.map +4 -4
  6. package/dist/bin/preview.js +43 -8
  7. package/dist/bin/preview.js.map +2 -2
  8. package/dist/web/app.js +11 -6
  9. package/dist/web/widgets.css +59 -28
  10. package/dist/web/widgets.js +230 -76
  11. package/package.json +3 -3
  12. package/resources/README.md +6 -50
  13. package/resources/buddy-creator/INSTALL.md +0 -25
  14. package/resources/buddy-creator/PATCHES.md +0 -53
  15. package/resources/buddy-creator/SKILL.md +0 -76
  16. package/resources/buddy-creator/THIRD_PARTY_NOTICES.md +0 -154
  17. package/resources/buddy-creator/agents/openai.yaml +0 -4
  18. package/resources/buddy-creator/assets/licenses/bail.txt +0 -26
  19. package/resources/buddy-creator/assets/licenses/ccount.txt +0 -26
  20. package/resources/buddy-creator/assets/licenses/character-entities-html4.txt +0 -26
  21. package/resources/buddy-creator/assets/licenses/character-entities-legacy.txt +0 -26
  22. package/resources/buddy-creator/assets/licenses/character-entities.txt +0 -26
  23. package/resources/buddy-creator/assets/licenses/character-reference-invalid.txt +0 -26
  24. package/resources/buddy-creator/assets/licenses/comma-separated-tokens.txt +0 -26
  25. package/resources/buddy-creator/assets/licenses/debug.txt +0 -24
  26. package/resources/buddy-creator/assets/licenses/decode-named-character-reference.txt +0 -26
  27. package/resources/buddy-creator/assets/licenses/dequal.txt +0 -25
  28. package/resources/buddy-creator/assets/licenses/devlop.txt +0 -26
  29. package/resources/buddy-creator/assets/licenses/escape-string-regexp.txt +0 -13
  30. package/resources/buddy-creator/assets/licenses/estree-util-is-identifier-name.txt +0 -26
  31. package/resources/buddy-creator/assets/licenses/extend.txt +0 -27
  32. package/resources/buddy-creator/assets/licenses/hast-util-to-jsx-runtime.txt +0 -26
  33. package/resources/buddy-creator/assets/licenses/hast-util-whitespace.txt +0 -26
  34. package/resources/buddy-creator/assets/licenses/html-url-attributes.txt +0 -25
  35. package/resources/buddy-creator/assets/licenses/inline-style-parser.txt +0 -13
  36. package/resources/buddy-creator/assets/licenses/is-alphabetical.txt +0 -26
  37. package/resources/buddy-creator/assets/licenses/is-alphanumerical.txt +0 -26
  38. package/resources/buddy-creator/assets/licenses/is-decimal.txt +0 -26
  39. package/resources/buddy-creator/assets/licenses/is-hexadecimal.txt +0 -26
  40. package/resources/buddy-creator/assets/licenses/is-plain-obj.txt +0 -13
  41. package/resources/buddy-creator/assets/licenses/longest-streak.txt +0 -26
  42. package/resources/buddy-creator/assets/licenses/markdown-table.txt +0 -26
  43. package/resources/buddy-creator/assets/licenses/mdast-util-find-and-replace.txt +0 -26
  44. package/resources/buddy-creator/assets/licenses/mdast-util-from-markdown.txt +0 -26
  45. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-autolink-literal.txt +0 -26
  46. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-footnote.txt +0 -26
  47. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-strikethrough.txt +0 -26
  48. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-table.txt +0 -26
  49. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-task-list-item.txt +0 -26
  50. package/resources/buddy-creator/assets/licenses/mdast-util-gfm.txt +0 -26
  51. package/resources/buddy-creator/assets/licenses/mdast-util-mdx-expression.txt +0 -26
  52. package/resources/buddy-creator/assets/licenses/mdast-util-mdx-jsx.txt +0 -26
  53. package/resources/buddy-creator/assets/licenses/mdast-util-mdxjs-esm.txt +0 -26
  54. package/resources/buddy-creator/assets/licenses/mdast-util-phrasing.txt +0 -27
  55. package/resources/buddy-creator/assets/licenses/mdast-util-to-hast.txt +0 -26
  56. package/resources/buddy-creator/assets/licenses/mdast-util-to-markdown.txt +0 -26
  57. package/resources/buddy-creator/assets/licenses/mdast-util-to-string.txt +0 -26
  58. package/resources/buddy-creator/assets/licenses/micromark-core-commonmark.txt +0 -26
  59. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-autolink-literal.txt +0 -26
  60. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-footnote.txt +0 -26
  61. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-strikethrough.txt +0 -26
  62. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-table.txt +0 -26
  63. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-tagfilter.txt +0 -26
  64. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-task-list-item.txt +0 -26
  65. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm.txt +0 -26
  66. package/resources/buddy-creator/assets/licenses/micromark-factory-destination.txt +0 -26
  67. package/resources/buddy-creator/assets/licenses/micromark-factory-label.txt +0 -26
  68. package/resources/buddy-creator/assets/licenses/micromark-factory-space.txt +0 -26
  69. package/resources/buddy-creator/assets/licenses/micromark-factory-title.txt +0 -26
  70. package/resources/buddy-creator/assets/licenses/micromark-factory-whitespace.txt +0 -26
  71. package/resources/buddy-creator/assets/licenses/micromark-util-character.txt +0 -26
  72. package/resources/buddy-creator/assets/licenses/micromark-util-chunked.txt +0 -26
  73. package/resources/buddy-creator/assets/licenses/micromark-util-classify-character.txt +0 -26
  74. package/resources/buddy-creator/assets/licenses/micromark-util-combine-extensions.txt +0 -26
  75. package/resources/buddy-creator/assets/licenses/micromark-util-decode-numeric-character-reference.txt +0 -26
  76. package/resources/buddy-creator/assets/licenses/micromark-util-decode-string.txt +0 -26
  77. package/resources/buddy-creator/assets/licenses/micromark-util-encode.txt +0 -26
  78. package/resources/buddy-creator/assets/licenses/micromark-util-html-tag-name.txt +0 -26
  79. package/resources/buddy-creator/assets/licenses/micromark-util-normalize-identifier.txt +0 -26
  80. package/resources/buddy-creator/assets/licenses/micromark-util-resolve-all.txt +0 -26
  81. package/resources/buddy-creator/assets/licenses/micromark-util-sanitize-uri.txt +0 -26
  82. package/resources/buddy-creator/assets/licenses/micromark-util-subtokenize.txt +0 -26
  83. package/resources/buddy-creator/assets/licenses/micromark-util-symbol.txt +0 -26
  84. package/resources/buddy-creator/assets/licenses/micromark-util-types.txt +0 -26
  85. package/resources/buddy-creator/assets/licenses/micromark.txt +0 -26
  86. package/resources/buddy-creator/assets/licenses/ms.txt +0 -25
  87. package/resources/buddy-creator/assets/licenses/parse-entities.txt +0 -26
  88. package/resources/buddy-creator/assets/licenses/phosphor-icons--react.txt +0 -25
  89. package/resources/buddy-creator/assets/licenses/property-information.txt +0 -26
  90. package/resources/buddy-creator/assets/licenses/react-dom.txt +0 -25
  91. package/resources/buddy-creator/assets/licenses/react-markdown.txt +0 -25
  92. package/resources/buddy-creator/assets/licenses/react.txt +0 -25
  93. package/resources/buddy-creator/assets/licenses/remark-gfm.txt +0 -26
  94. package/resources/buddy-creator/assets/licenses/remark-parse.txt +0 -25
  95. package/resources/buddy-creator/assets/licenses/remark-rehype.txt +0 -26
  96. package/resources/buddy-creator/assets/licenses/remark-stringify.txt +0 -25
  97. package/resources/buddy-creator/assets/licenses/scheduler.txt +0 -25
  98. package/resources/buddy-creator/assets/licenses/space-separated-tokens.txt +0 -26
  99. package/resources/buddy-creator/assets/licenses/stringify-entities.txt +0 -26
  100. package/resources/buddy-creator/assets/licenses/style-to-js.txt +0 -26
  101. package/resources/buddy-creator/assets/licenses/style-to-object.txt +0 -26
  102. package/resources/buddy-creator/assets/licenses/trim-lines.txt +0 -26
  103. package/resources/buddy-creator/assets/licenses/trough.txt +0 -25
  104. package/resources/buddy-creator/assets/licenses/types--debug.txt +0 -25
  105. package/resources/buddy-creator/assets/licenses/types--estree-jsx.txt +0 -25
  106. package/resources/buddy-creator/assets/licenses/types--estree.txt +0 -25
  107. package/resources/buddy-creator/assets/licenses/types--hast.txt +0 -25
  108. package/resources/buddy-creator/assets/licenses/types--mdast.txt +0 -25
  109. package/resources/buddy-creator/assets/licenses/types--ms.txt +0 -25
  110. package/resources/buddy-creator/assets/licenses/types--unist.txt +0 -25
  111. package/resources/buddy-creator/assets/licenses/ungap--structured-clone.txt +0 -19
  112. package/resources/buddy-creator/assets/licenses/unified.txt +0 -25
  113. package/resources/buddy-creator/assets/licenses/unist-util-is.txt +0 -26
  114. package/resources/buddy-creator/assets/licenses/unist-util-position.txt +0 -26
  115. package/resources/buddy-creator/assets/licenses/unist-util-stringify-position.txt +0 -26
  116. package/resources/buddy-creator/assets/licenses/unist-util-visit-parents.txt +0 -26
  117. package/resources/buddy-creator/assets/licenses/unist-util-visit.txt +0 -26
  118. package/resources/buddy-creator/assets/licenses/vfile-message.txt +0 -26
  119. package/resources/buddy-creator/assets/licenses/vfile.txt +0 -25
  120. package/resources/buddy-creator/assets/licenses/zwitch.txt +0 -26
  121. package/resources/buddy-creator/assets/preview/assets/index-B5VmkoSx.js +0 -39
  122. package/resources/buddy-creator/assets/preview/assets/index-DWKP6b-3.css +0 -4
  123. package/resources/buddy-creator/assets/preview/brand-logo.png +0 -0
  124. package/resources/buddy-creator/assets/preview/brand.css +0 -100
  125. package/resources/buddy-creator/assets/preview/index.html +0 -7
  126. package/resources/buddy-creator/assets/preview/reader.css +0 -165
  127. package/resources/buddy-creator/assets/preview/reader.js +0 -135
  128. package/resources/buddy-creator/assets/preview/widgets.css +0 -1
  129. package/resources/buddy-creator/assets/preview/widgets.js +0 -168
  130. package/resources/buddy-creator/assets/widget-reference/conversation.png +0 -0
  131. package/resources/buddy-creator/assets/widget-reference/expanded.png +0 -0
  132. package/resources/buddy-creator/assets/widget-v2/app.js +0 -62
  133. package/resources/buddy-creator/assets/widget-v2/style.css +0 -1
  134. package/resources/buddy-creator/assets/widget-v3/app.js +0 -65
  135. package/resources/buddy-creator/assets/widget-v3/style.css +0 -103
  136. package/resources/buddy-creator/assets/widget-v4/app.js +0 -66
  137. package/resources/buddy-creator/assets/widget-v4/style.css +0 -187
  138. package/resources/buddy-creator/references/artifact-schema.md +0 -70
  139. package/resources/buddy-creator/references/catalog.json +0 -401
  140. package/resources/buddy-creator/references/definition.md +0 -24
  141. package/resources/buddy-creator/references/dialogue-examples.md +0 -133
  142. package/resources/buddy-creator/references/host-guide.md +0 -219
  143. package/resources/buddy-creator/references/interview.md +0 -235
  144. package/resources/buddy-creator/references/knowledge.md +0 -40
  145. package/resources/buddy-creator/references/methods.md +0 -77
  146. package/resources/buddy-creator/references/opening.md +0 -14
  147. package/resources/buddy-creator/references/preview-panel.md +0 -66
  148. package/resources/buddy-creator/references/recovery.md +0 -46
  149. package/resources/buddy-creator/references/service.md +0 -76
  150. package/resources/buddy-creator/references/widgets-protocol.md +0 -83
  151. package/resources/buddy-creator/references/widgets.md +0 -99
  152. package/resources/buddy-creator/scripts/buddy.py +0 -108
  153. package/resources/buddy-creator/scripts/buddy_client.py +0 -67
  154. package/resources/buddy-creator/scripts/buddy_core.py +0 -865
  155. package/resources/buddy-creator/scripts/completion.py +0 -296
  156. package/resources/buddy-creator/scripts/interview.py +0 -215
  157. package/resources/buddy-creator/scripts/preview.py +0 -617
  158. package/resources/buddy-creator/scripts/preview_panel.py +0 -80
  159. package/resources/buddy-creator/scripts/sources.py +0 -239
  160. package/resources/buddy-creator/scripts/widget_render.py +0 -66
  161. package/resources/buddy-creator/scripts/widget_render_v1.py +0 -176
  162. package/resources/buddy-creator/scripts/widget_render_v2.py +0 -98
  163. package/resources/buddy-creator/scripts/widget_render_v3.py +0 -86
  164. package/resources/buddy-creator/scripts/widget_render_v4.py +0 -121
  165. package/resources/buddy-creator/scripts/widgets.py +0 -227
  166. package/resources/buddy-creator/version.json +0 -8
  167. package/resources/buddy-creator.manifest.json +0 -791
  168. package/resources/dada-object-avatar/SKILL.md +0 -37
  169. package/resources/dada-object-avatar/agents/openai.yaml +0 -4
  170. package/resources/dada-object-avatar/assets/examples/cat.png +0 -0
  171. package/resources/dada-object-avatar/assets/examples/cooking.png +0 -0
  172. package/resources/dada-object-avatar/assets/examples/divination.png +0 -0
  173. package/resources/dada-object-avatar/assets/examples/doctor.png +0 -0
  174. package/resources/dada-object-avatar/assets/examples/relationship.png +0 -0
  175. package/resources/dada-object-avatar/assets/examples/work.png +0 -0
  176. package/resources/dada-object-avatar/assets/preview.png +0 -0
  177. package/resources/dada-object-avatar/references/evaluation-rubric.md +0 -22
  178. package/resources/dada-object-avatar/references/examples.json +0 -8
  179. package/resources/dada-object-avatar/references/prompt-template.md +0 -27
  180. package/resources/dada-object-avatar/references/style-system.md +0 -54
  181. package/resources/dada-object-avatar/references/subject-design.md +0 -23
  182. package/resources/dada-object-avatar/references/training-guide.md +0 -14
  183. package/resources/dada-object-avatar/references/training-log.jsonl +0 -0
  184. package/resources/dada-object-avatar/references/training-record.schema.json +0 -20
  185. package/resources/dada-object-avatar/scripts/choose_gaze.py +0 -36
  186. package/resources/dada-object-avatar/scripts/record_example.py +0 -22
  187. package/resources/dada-object-avatar/scripts/validate_png.py +0 -52
  188. package/resources/dada-object-avatar.manifest.json +0 -85
@@ -1,617 +0,0 @@
1
- """Read-only, local preview for Buddy Creator Skill.
2
-
3
- The bundled browser application is prebuilt static JavaScript: running it in a
4
- browser does not require Node.js. Business writes belong exclusively to core.py.
5
- """
6
-
7
- import contextlib
8
- import datetime
9
- import hashlib
10
- import json
11
- import mimetypes
12
- import os
13
- from pathlib import Path
14
- import re
15
- import secrets
16
- import socket
17
- import subprocess
18
- import sys
19
- import threading
20
- import time
21
- from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
22
- from urllib.error import HTTPError, URLError
23
- from urllib.parse import unquote, urlsplit
24
- from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener
25
-
26
-
27
- ROOT = Path(__file__).resolve().parent.parent
28
- RUNTIME_VERSION = "buddy-creator-" + json.loads((ROOT / "version.json").read_text(encoding="utf-8"))["version"]
29
- COMPATIBLE_RUNTIME_VERSIONS = {RUNTIME_VERSION}
30
- STAGES = ("definition", "knowledge", "methods", "service")
31
- STAGE_LABELS = dict(zip(STAGES, ("定义", "知识", "方法", "服务")))
32
- LABELS = {"confirmed": "已确认", "accepted": "已采纳", "rejected": "未采纳",
33
- "pending": "待确认", "revised": "待重新确认"}
34
- PATHS = {"acquisition-paid": "体验后订阅", "acquisition-maintenance": "暂不订阅",
35
- "paid-paid": "续费", "paid-maintenance": "停止续费", "maintenance-paid": "恢复订阅"}
36
-
37
-
38
- def _read(path):
39
- with Path(path).open("r", encoding="utf-8") as handle:
40
- return json.load(handle)
41
-
42
-
43
- def _optional(path):
44
- try:
45
- return _read(path)
46
- except FileNotFoundError:
47
- return None
48
-
49
-
50
- def _write(path, data):
51
- path = Path(path)
52
- temporary = path.with_name(path.name + "." + secrets.token_hex(8) + ".tmp")
53
- try:
54
- descriptor = os.open(str(temporary), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
55
- with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
56
- json.dump(data, handle, ensure_ascii=False, separators=(",", ":"))
57
- handle.flush()
58
- os.fsync(handle.fileno())
59
- os.replace(str(temporary), str(path))
60
- if os.name != "nt":
61
- directory = os.open(str(path.parent), os.O_RDONLY)
62
- try:
63
- os.fsync(directory)
64
- finally:
65
- os.close(directory)
66
- finally:
67
- with contextlib.suppress(FileNotFoundError):
68
- temporary.unlink()
69
-
70
-
71
- def _fail(code, message):
72
- raise RuntimeError(code + ": " + message)
73
-
74
-
75
- def _decision(state, artifact, decision="confirmed"):
76
- return any(c.get("objectId") == artifact["id"] and c.get("hash") == artifact.get("hash")
77
- and not c.get("invalidatedBy") and c.get("decision") == decision
78
- for c in state.get("confirmations", []))
79
-
80
-
81
- def _status(state, artifact):
82
- for decision in ("confirmed", "accepted", "rejected"):
83
- if _decision(state, artifact, decision):
84
- return decision
85
- return "revised" if any(c.get("objectId") == artifact["id"]
86
- for c in state.get("confirmations", [])) else "pending"
87
-
88
-
89
- def _book_confirmed(state, stage, chapters):
90
- artifacts = state.get("artifacts", {})
91
- return bool(chapters[stage]) and all(
92
- artifacts.get(stage + "." + str(i + 1))
93
- and _decision(state, artifacts[stage + "." + str(i + 1)])
94
- for i in range(len(chapters[stage])))
95
-
96
-
97
- def _object_for_target(state, target_id):
98
- candidate = None
99
- if re.fullmatch(r"H\d+", target_id):
100
- candidate = "hypothesis.H" + str(int(target_id[1:]))
101
- elif re.fullmatch(r"[ME]\d+", target_id):
102
- candidate = "scenario." + target_id
103
- elif target_id.startswith("T."):
104
- candidate = "transition." + target_id.split(".")[1]
105
- elif re.fullmatch(r"D\d+", target_id):
106
- candidate = "definition." + str(int(target_id[1:]))
107
- return candidate if candidate in state.get("artifacts", {}) else None
108
-
109
-
110
- def _phase(target_id, stage):
111
- target_id = target_id or ""
112
- if target_id.startswith(("hypothesis.", "H")):
113
- return "候选校准"
114
- if target_id.startswith(("scenario.M", "M")):
115
- return "基础情境"
116
- if target_id.startswith(("scenario.E", "E")):
117
- return "拓展情境"
118
- if target_id.startswith(("T.", "transition.")):
119
- return "用户路径"
120
- if target_id == "service.blueprint":
121
- return "服务模式确认"
122
- if target_id == "R00":
123
- return "本次优化"
124
- if target_id == "S00":
125
- return "规划方式"
126
- if re.fullmatch(r"(definition|knowledge|methods|service)\.\d+", target_id):
127
- return "手册确认"
128
- return {"definition": "创作定义", "knowledge": "知识整理",
129
- "methods": "候选校准", "service": "服务规划"}[stage]
130
-
131
-
132
- def _artifact_view(state, artifact):
133
- # Do not expose dependencies, raw turn input, archives, local paths or host
134
- # context. Only authored public content and its selected citations are sent.
135
- view = {key: artifact.get(key) for key in ("id", "stage", "kind", "title", "hash")}
136
- view.update(markdown=artifact.get("markdown", ""), data=artifact.get("data") or {},
137
- unresolved=artifact.get("unresolved") or [], status=_status(state, artifact))
138
- view["evidence"] = [{key: ref[key] for key in ("type", "id", "hash", "locator", "quote")
139
- if key in ref} for ref in artifact.get("evidence", [])]
140
- return view
141
-
142
-
143
- def _time(value):
144
- try:
145
- return datetime.datetime.fromisoformat(str(value).replace("Z", "+00:00")).timestamp()
146
- except (ValueError, TypeError, OverflowError):
147
- return 0
148
-
149
-
150
- def _activity(state, drafts):
151
- if state.get("paused"):
152
- return {"mode": "paused", "label": "已暂停"}
153
- pending = state.get("turns", {}).get(state.get("pendingTurnId"))
154
- if pending and pending.get("status") == "working":
155
- last_signal = max([_time(pending.get("updatedAt") or pending.get("createdAt"))]
156
- + [_time(d.get("updatedAt")) for d in drafts])
157
- elapsed = max(0, time.time() - last_signal)
158
- if not last_signal or elapsed >= 120:
159
- return {"mode": "waiting", "label": "本轮还未完成", "detail": "回主对话查看进展"}
160
- result = {"mode": "working", "label": "正在整理手册" if drafts else "正在梳理你的想法"}
161
- if elapsed >= 30:
162
- result["detail"] = "可先阅读已有内容"
163
- return result
164
- selected = state.get("sourcePlan", {}).get("sourceIds", [])
165
- if any(s.get("status") == "failed" for s in state.get("sources", {}).values() if s.get("id") in selected):
166
- return {"mode": "attention", "label": "资料需要处理", "detail": "回主对话查看原因"}
167
- current_delivery = state.get("currentDeliveryId")
168
- if current_delivery and current_delivery not in state.get("presentations", []):
169
- return {"mode": "ready", "label": "回复已整理", "detail": "等待主对话展示"}
170
- return None
171
-
172
-
173
- def snapshot(workspace, state=None):
174
- workspace = Path(workspace)
175
- state = state if state is not None else _read(workspace / "state.json")
176
- catalog = _read(ROOT / "references" / "catalog.json")
177
- cards, chapters = catalog["cards"], catalog["chapters"]
178
- stage = state["stage"]
179
- targets = state.get("targets", {})
180
- artifacts = [_artifact_view(state, a) for a in state.get("artifacts", {}).values() if a['stage'] in STAGES]
181
- by_id = {a["id"]: a for a in artifacts}
182
- delivery = state.get("deliveries", {}).get(state.get("currentDeliveryId"), {})
183
- if delivery.get("resumeDeliveryId"):
184
- delivery = state.get("deliveries", {}).get(delivery["resumeDeliveryId"], delivery)
185
- target_id = (delivery.get("question") or {}).get("targetId")
186
- if cards.get(target_id, {}).get("stage") != stage:
187
- target_id = None
188
- confirmation = delivery.get("confirmationTarget") or {}
189
- requested = [r["id"] for r in confirmation.get("objects", [])
190
- if confirmation.get("stage") == stage and by_id.get(r.get("id"), {}).get("hash") == r.get("hash")]
191
- hypotheses = sorted([a for a in artifacts if a["kind"] == "hypothesis"], key=lambda a: a["id"])
192
- target_object = _object_for_target(state, target_id) if target_id else None
193
- complete = all(_book_confirmed(state, s, chapters) for s in STAGES)
194
- focus_ids = [target_object] if target_object else [a_id for a_id in requested
195
- if by_id[a_id]["status"] in ("pending", "revised")]
196
- if not focus_ids and not target_id:
197
- candidate = next((a for a in artifacts if a["stage"] == stage and a["status"] in ("pending", "revised")), None)
198
- if candidate:
199
- focus_ids = [candidate["id"]]
200
- focus = by_id.get(focus_ids[0]) if focus_ids else None
201
- first_target = next((i for i, c in cards.items() if c["stage"] == stage
202
- and not i.startswith(("H", "T.", "R00"))
203
- and targets.get(i, {}).get("status", "unstarted") == "unstarted"), None)
204
- current_target = target_id or (first_target if not focus and not complete
205
- and not (stage == "methods" and not hypotheses) else None)
206
- topic_title = cards.get(current_target, {}).get("title")
207
- if current_target and current_target.startswith("T."):
208
- pieces = current_target.split(".")
209
- topic_title = PATHS.get(pieces[1], "用户路径") + " / " + ("时点", "权益", "表达")[int(pieces[2]) - 1]
210
- def target_summary(identifier):
211
- item = targets.get(identifier, {})
212
- summary = item.get("summary", "")
213
- gaps = item.get("gaps", [])
214
- return summary + (("\n\n" if summary else "") + "待补:" + ";".join(gaps) if gaps else "")
215
-
216
- current = {
217
- "stage": stage,
218
- "phase": "创作完成" if complete and not target_id and not focus else _phase(current_target or (focus or {}).get("id"), stage),
219
- "title": STAGE_LABELS[stage] + "手册" if len(focus_ids) > 1 else
220
- (focus or {}).get("title") or topic_title or ("四本手册已完成" if complete else "整理方法候选" if stage == "methods" else "整理当前内容"),
221
- "targetId": current_target,
222
- "artifactIds": focus_ids,
223
- "summary": target_summary(current_target) if current_target else delivery.get("blocked", ""),
224
- "status": "已暂停" if state.get("paused") else "待补充" if delivery.get("blocked") else LABELS[focus["status"]] if focus else "已完成" if complete else "讨论中",
225
- }
226
- if focus and focus["kind"] == "hypothesis":
227
- current["position"] = {"index": next(i + 1 for i, a in enumerate(hypotheses) if a["id"] == focus["id"]), "total": len(hypotheses)}
228
-
229
- def topic(identifier):
230
- object_id = _object_for_target(state, identifier)
231
- artifact = by_id.get(object_id)
232
- target = targets.get(identifier, {})
233
- return {"id": identifier,
234
- "title": artifact["title"] if artifact and artifact["kind"] == "hypothesis" else cards.get(identifier, {}).get("title", identifier),
235
- "summary": target_summary(identifier), "artifactId": object_id,
236
- "current": identifier == current_target or bool(object_id and object_id in focus_ids),
237
- "status": LABELS[artifact["status"]] if artifact else "已记录" if target.get("status") == "sufficient"
238
- else "已跳过" if target.get("status") == "skipped" else "待补充" if target.get("summary") or target.get("status") in ("uncertain", "exhausted") else "未开始"}
239
-
240
- stages = []
241
- for index, stage_id in enumerate(STAGES):
242
- identifiers = [i for i, c in cards.items() if c["stage"] == stage_id]
243
- groups = []
244
- if stage_id == "methods":
245
- groups.extend([
246
- {"id": "candidates", "title": "方法候选", "topics": [topic("H" + a["id"].split("H")[-1].zfill(2)) for a in hypotheses]},
247
- {"id": "base", "title": "基础情境", "topics": [topic(i) for i in identifiers if i.startswith("M")]},
248
- {"id": "extended", "title": "拓展情境", "topics": [topic(i) for i in identifiers if i.startswith("E")]},
249
- ])
250
- elif stage_id == "service":
251
- groups.append({"id": "planning", "title": "服务规划", "topics": [topic(i) for i in identifiers if i.startswith("S")]})
252
- path_topics = []
253
- for path_id, title in PATHS.items():
254
- artifact = by_id.get("transition." + path_id)
255
- path_topics.append({"id": "path." + path_id, "title": title, "summary": "",
256
- "artifactId": artifact["id"] if artifact else None,
257
- "current": bool(current_target and current_target.startswith("T." + path_id + ".")) or "transition." + path_id in focus_ids,
258
- "status": LABELS[artifact["status"]] if artifact else "系统固定" if path_id == "maintenance-paid" else "未开始"})
259
- groups.append({"id": "paths", "title": "用户路径", "topics": path_topics})
260
- elif stage_id == "knowledge":
261
- groups.append({"id": "interview", "title": "知识整理", "topics": [topic(i) for i in identifiers]})
262
- book_topics = []
263
- for chapter_index, title in enumerate(chapters[stage_id]):
264
- identifier = stage_id + "." + str(chapter_index + 1)
265
- artifact = by_id.get(identifier)
266
- target_key = "D" + str(chapter_index + 1).zfill(2) if stage_id == "definition" else None
267
- target = targets.get(target_key, {})
268
- book_topics.append({"id": identifier, "title": title, "summary": target_summary(target_key) if target_key else "",
269
- "artifactId": artifact["id"] if artifact else None,
270
- "current": identifier in focus_ids or bool(target_key and target_key == current_target),
271
- "status": LABELS[artifact["status"]] if artifact else "已记录" if target.get("status") == "sufficient" else "待补充" if target.get("summary") else "待形成"})
272
- groups.append({"id": "booklet", "title": "Booklet", "topics": book_topics})
273
- members = [a for a in artifacts if a["stage"] == stage_id]
274
- status = ("已确认" if _book_confirmed(state, stage_id, chapters)
275
- else "待重新确认" if any(a["status"] == "revised" for a in members)
276
- else "待确认" if any(a["kind"] == "chapter" for a in members)
277
- else "进行中" if stage_id == stage or any(targets.get(i, {}).get("status", "unstarted") != "unstarted" for i in identifiers)
278
- else "未开始")
279
- stages.append({"id": stage_id, "number": index + 1, "title": STAGE_LABELS[stage_id],
280
- "current": stage_id == stage, "status": status, "groups": groups,
281
- "confirmedChapters": sum(a["kind"] == "chapter" and a["status"] == "confirmed" for a in members),
282
- "totalChapters": len(chapters[stage_id])})
283
- pending_id = state.get("pendingTurnId")
284
- drafts = [d for d in state.get("drafts", []) if pending_id and d.get("turnId") == pending_id
285
- and state.get("turns", {}).get(pending_id, {}).get("status") == "working"]
286
- drafts.sort(key=lambda d: d.get("sequence", 0))
287
- visible_drafts = []
288
- for draft in drafts[-1:]:
289
- visible_drafts.append({"id": draft.get("id", "draft." + pending_id), "stepId": pending_id,
290
- "title": draft.get("title", "手册草稿"), "markdown": draft.get("markdown", ""),
291
- "hash": draft.get("hash") or hashlib.sha256(draft.get("markdown", "").encode("utf-8")).hexdigest()})
292
- sources = []
293
- for source in state.get("sources", {}).values():
294
- # Source processing error objects may contain local paths. Keep the
295
- # browser message actionable but generic; details stay in the main chat.
296
- sources.append({"id": source["id"], "title": source.get("title", "知识资料"),
297
- "kind": source.get("kind", "file"), "status": source.get("status", "failed"),
298
- "version": source.get("version"), "warnings": [],
299
- "error": "资料尚未完整处理,请在主对话中补充或重试。" if source.get("status") == "failed" else None})
300
- completion_view = None
301
- try:
302
- import completion
303
- completion_view = completion.snapshot(workspace, state)
304
- except (OSError, ValueError, ImportError):
305
- if complete:
306
- completion_view = {"revision": state["revision"], "status": "failed", "message": "四册已确认,成果暂时无法读取。"}
307
- import widgets
308
- widget_design = widgets.projection(state, workspace)
309
- if complete and widgets.enabled(state) and not widgets.ready(state):
310
- current.update(phase='小挂件交互设计', title=widget_design['nextStep'], status='待确认')
311
- return {"buddyId": state["buddyId"], "revision": state["revision"], "stage": stage,
312
- "paused": bool(state.get("paused")), "activity": _activity(state, drafts),
313
- "completion": completion_view, "widgetDesign": widget_design, "current": current, "stages": stages,
314
- "artifacts": artifacts, "sources": sources, "drafts": visible_drafts}
315
-
316
-
317
- def _endpoint(workspace, workspace_id):
318
- endpoint = _optional(workspace / "preview-endpoint.json")
319
- if endpoint is not None and not (isinstance(endpoint, dict) and endpoint.get("version") == 1
320
- and endpoint.get("workspaceId") == workspace_id and type(endpoint.get("port")) is int
321
- and 1 <= endpoint["port"] <= 65535
322
- and isinstance(endpoint.get("token"), str) and re.fullmatch(r"[a-f0-9]{48}", endpoint["token"])):
323
- _fail("PREVIEW_ADDRESS_INVALID", "固定预览地址无效或属于其他搭子;已保留原记录。")
324
- return endpoint
325
-
326
-
327
- def _url(endpoint):
328
- return "http://127.0.0.1:%s/p/%s/" % (endpoint["port"], endpoint["token"])
329
-
330
-
331
- def _listening(port):
332
- try:
333
- with socket.create_connection(("127.0.0.1", port), timeout=0.5):
334
- return True
335
- except OSError:
336
- return False
337
-
338
-
339
- def _identity(endpoint, workspace_id, record):
340
- class NoRedirect(HTTPRedirectHandler):
341
- def redirect_request(self, request, response, code, message, headers, new_url):
342
- return None
343
-
344
- try:
345
- # Loopback health requests must not pass through configured HTTP proxies.
346
- with build_opener(ProxyHandler({}), NoRedirect()).open(Request(_url(endpoint) + "api/health"), timeout=1.2) as response:
347
- if response.geturl() != _url(endpoint) + "api/health":
348
- _fail("PREVIEW_IDENTITY_MISMATCH", "预览身份不匹配,未接管此端口。")
349
- identity = json.loads(response.read(8192).decode("utf-8"))
350
- except (OSError, HTTPError, URLError, ValueError):
351
- return False
352
- if (isinstance(identity, dict) and identity.get("workspaceId") == workspace_id
353
- and record and identity.get("pid") == record.get("pid") and record.get("workspaceId") == workspace_id
354
- and record.get("url") == _url(endpoint)
355
- and identity.get("runtimeVersion") in {"buddy-creator-1.3.1", "buddy-creator-1.3.0", "buddy-creator-1.2.0", "buddy-creator-1.1.0", "buddy-creator-1.0.0", "python-trial-1"}):
356
- _fail("PREVIEW_UPGRADE_REQUIRED", "当前作品的旧版预览服务仍在运行。由宿主核对该服务后重启,再 open 原工作区;保留固定地址、确认记录和一次性面板提醒。")
357
- if not isinstance(identity, dict) or identity.get("workspaceId") != workspace_id or identity.get("runtimeVersion") not in COMPATIBLE_RUNTIME_VERSIONS:
358
- _fail("PREVIEW_IDENTITY_MISMATCH", "端口上的服务不属于当前版本的搭子预览,未接管或停止它。")
359
- if not record or identity.get("pid") != record.get("pid") or record.get("workspaceId") != workspace_id or record.get("url") != _url(endpoint):
360
- _fail("PREVIEW_IDENTITY_MISMATCH", "预览进程记录与端口上的服务不一致,未接管或停止它。")
361
- return True
362
-
363
-
364
- def _alive(pid):
365
- if type(pid) is not int or pid <= 0:
366
- return True
367
- try:
368
- os.kill(pid, 0)
369
- return True
370
- except ProcessLookupError:
371
- return False
372
- except (PermissionError, OSError):
373
- return True
374
-
375
-
376
- @contextlib.contextmanager
377
- def _lock(workspace, name):
378
- path = workspace / name
379
- owner = {"pid": os.getpid(), "token": secrets.token_hex(12)}
380
- deadline = time.monotonic() + 12
381
- while True:
382
- try:
383
- descriptor = os.open(str(path), os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
384
- with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
385
- json.dump(owner, handle)
386
- handle.flush()
387
- os.fsync(handle.fileno())
388
- break
389
- except FileExistsError:
390
- try:
391
- observed = _read(path)
392
- if not _alive(observed.get("pid")) and _read(path) == observed:
393
- path.unlink()
394
- continue
395
- except (OSError, ValueError, AttributeError):
396
- pass
397
- if time.monotonic() >= deadline:
398
- _fail("PREVIEW_BUSY", "预览启动正在进行或锁记录需检查,请稍后重试。")
399
- time.sleep(0.1)
400
- try:
401
- yield
402
- finally:
403
- with contextlib.suppress(OSError, ValueError):
404
- if _read(path) == owner:
405
- path.unlink()
406
-
407
-
408
- def start(workspace):
409
- workspace = Path(workspace).resolve()
410
- state = _read(workspace / "state.json")
411
- workspace_id = state["workspaceId"]
412
- with _lock(workspace, ".preview-lifecycle.lock"):
413
- endpoint = _endpoint(workspace, workspace_id)
414
- record = _optional(workspace / "preview-server.json")
415
- if endpoint and _identity(endpoint, workspace_id, record):
416
- return {"url": _url(endpoint), "readonly": True, "addressPolicy": "stable_per_workspace"}
417
- if endpoint and _listening(endpoint["port"]):
418
- _fail("PREVIEW_PORT_IN_USE", "当前搭子的固定端口已被其他进程占用;保留原地址,未切换端口或停止其他程序。")
419
- arguments = [sys.executable, str(ROOT / "scripts" / "buddy.py"), "serve", "--workspace", str(workspace)]
420
- options = {"stdin": subprocess.DEVNULL, "stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL,
421
- "cwd": str(ROOT), "close_fds": True}
422
- if os.name == "nt":
423
- options["creationflags"] = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
424
- else:
425
- options["start_new_session"] = True
426
- process = subprocess.Popen(arguments, **options)
427
- for _ in range(80):
428
- time.sleep(0.1)
429
- if process.poll() is not None:
430
- _fail("PREVIEW_START_FAILED", "本地预览未启动。可在主对话中重试打开;固定地址与已保存内容保持不变。")
431
- endpoint = _endpoint(workspace, workspace_id)
432
- record = _optional(workspace / "preview-server.json")
433
- if endpoint and record and record.get("pid") == process.pid and _identity(endpoint, workspace_id, record):
434
- return {"url": _url(endpoint), "readonly": True, "addressPolicy": "stable_per_workspace"}
435
- # Only stop the still-running child created by this invocation. Never
436
- # signal a PID recovered from disk or another listener on the port.
437
- if process.poll() is None:
438
- process.terminate()
439
- _fail("PREVIEW_START_FAILED", "本地预览启动超时;请稍后重试,固定地址已保留。")
440
-
441
-
442
- def serve(workspace):
443
- workspace = Path(workspace).resolve()
444
- state = _read(workspace / "state.json")
445
- workspace_id = state["workspaceId"]
446
- assets = (ROOT / "assets" / "preview").resolve()
447
- if not (assets / "index.html").is_file():
448
- _fail("PREVIEW_ASSETS_MISSING", "安装包中的预览文件缺失,请重新解压完整安装包。")
449
- endpoint = _endpoint(workspace, workspace_id)
450
- token = endpoint["token"] if endpoint else secrets.token_hex(24)
451
- base = "/p/" + token + "/"
452
- streams = threading.BoundedSemaphore(16)
453
-
454
- class Handler(BaseHTTPRequestHandler):
455
- protocol_version = "HTTP/1.1"
456
- server_version = "BuddyPreview"
457
- sys_version = ""
458
-
459
- def log_message(self, format, *args):
460
- pass
461
-
462
- def send_error(self, code, message=None, explain=None):
463
- self.reply(code, "请求无法处理。".encode("utf-8"), "text/plain; charset=utf-8")
464
-
465
- def reply(self, code, body=b"", content_type="text/plain; charset=utf-8"):
466
- self.send_response(code)
467
- self.headers_for(content_type)
468
- self.send_header("Content-Length", str(len(body)))
469
- self.send_header("Connection", "close")
470
- self.end_headers()
471
- self.wfile.write(body)
472
- self.close_connection = True
473
-
474
- def headers_for(self, content_type):
475
- self.send_header("Content-Type", content_type)
476
- self.send_header("Cache-Control", "no-store")
477
- self.send_header("X-Content-Type-Options", "nosniff")
478
- self.send_header("Referrer-Policy", "no-referrer")
479
- frame = "'self'" if '/widget-assets/' in urlsplit(self.path).path else "'none'"
480
- self.send_header("Content-Security-Policy", "default-src 'self'; script-src 'self'; connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors " + frame)
481
-
482
- def do_GET(self):
483
- expected_host = "127.0.0.1:" + str(self.server.server_port)
484
- if self.headers.get("Host") != expected_host or self.headers.get("Origin") not in (None, "http://" + expected_host):
485
- self.reply(403)
486
- return
487
- route_path = urlsplit(self.path)
488
- if route_path.scheme or route_path.netloc or not route_path.path.startswith(base):
489
- self.reply(404)
490
- return
491
- route = unquote(route_path.path[len(base):])
492
- try:
493
- if route == "api/health":
494
- self.json_reply({"workspaceId": workspace_id, "pid": os.getpid(), "runtimeVersion": RUNTIME_VERSION})
495
- elif route == "api/snapshot":
496
- self.json_reply(snapshot(workspace))
497
- elif route == "api/events":
498
- self.events()
499
- elif route.startswith('widget-assets/'):
500
- import buddy_core
501
- import widgets
502
- import widget_render
503
- pieces = route.split('/')
504
- state = buddy_core.load(workspace)
505
- artifact = next((a for a in state['artifacts'].values() if a.get('kind') == 'widget-prototype' and len(pieces) == 3 and a['hash'] == pieces[1]), None)
506
- if not artifact or widgets.blockers(state, artifact):
507
- self.reply(404)
508
- return
509
- status = widget_render.asset_status(workspace, artifact, state)
510
- if not status['pass'] or pieces[2] not in {f['name'] for f in status.get('files', [])}:
511
- self.reply(404)
512
- return
513
- file = widget_render.directory(workspace, artifact) / pieces[2]
514
- self.reply(200, file.read_bytes(), {'.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.svg': 'image/svg+xml'}[file.suffix])
515
- elif route.startswith('deliverables/'):
516
- import buddy_core
517
- import completion
518
- state = buddy_core.load(workspace)
519
- checked = completion.return_to_caller(workspace, state, {})
520
- parts = route.split('/', 2)
521
- if checked.get('directive') != 'creator_complete' or len(parts) != 3 or parts[1] != state['revision']:
522
- self.reply(404)
523
- return
524
- directory = workspace / 'deliverables' / state['revision']
525
- inventory = _read(directory / 'MANIFEST.json')
526
- if parts[2] not in {entry['path'] for entry in inventory['files']}:
527
- self.reply(404)
528
- return
529
- file = directory / parts[2]
530
- self.reply(200, file.read_bytes(), mimetypes.guess_type(str(file))[0] or 'text/plain; charset=utf-8')
531
- else:
532
- file = (assets / (route or "index.html")).resolve()
533
- if assets not in file.parents or not file.is_file():
534
- self.reply(404)
535
- return
536
- content_type = {".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8",
537
- ".css": "text/css; charset=utf-8", ".svg": "image/svg+xml", ".woff2": "font/woff2"}.get(file.suffix, mimetypes.guess_type(str(file))[0] or "application/octet-stream")
538
- self.reply(200, file.read_bytes(), content_type)
539
- except (BrokenPipeError, ConnectionResetError, TimeoutError):
540
- self.close_connection = True
541
- except Exception:
542
- self.reply(503, "内容暂时无法读取,请保留页面并回到主对话检查。".encode("utf-8"))
543
-
544
- def json_reply(self, data):
545
- self.reply(200, json.dumps(data, ensure_ascii=False, separators=(",", ":")).encode("utf-8"), "application/json; charset=utf-8")
546
-
547
- def events(self):
548
- if not streams.acquire(blocking=False):
549
- self.reply(503)
550
- return
551
- try:
552
- self.send_response(200)
553
- self.headers_for("text/event-stream; charset=utf-8")
554
- self.send_header("Connection", "keep-alive")
555
- self.end_headers()
556
- self.connection.settimeout(15)
557
- self.wfile.write(b"event: ready\ndata: {}\n\n")
558
- self.wfile.flush()
559
- previous = None
560
- unavailable = False
561
- while True:
562
- try:
563
- current = hashlib.sha256(json.dumps(snapshot(workspace), ensure_ascii=False, sort_keys=True).encode("utf-8")).hexdigest()
564
- if current != previous or unavailable:
565
- payload = ('event: changed\ndata: {"version":"' + current + '"}\n\n').encode("utf-8")
566
- previous, unavailable = current, False
567
- else:
568
- payload = b": heartbeat\n\n"
569
- except Exception:
570
- payload = b"event: unavailable\ndata: {}\n\n"
571
- unavailable = True
572
- self.wfile.write(payload)
573
- self.wfile.flush()
574
- time.sleep(1)
575
- except (OSError, TimeoutError):
576
- self.close_connection = True
577
- finally:
578
- streams.release()
579
-
580
- def readonly(self):
581
- self.reply(405, "预览只读,请回到主对话修改。".encode("utf-8"))
582
-
583
- do_POST = readonly
584
- do_PUT = readonly
585
- do_PATCH = readonly
586
- do_DELETE = readonly
587
- do_HEAD = readonly
588
- do_OPTIONS = readonly
589
-
590
- class Server(ThreadingHTTPServer):
591
- daemon_threads = True
592
-
593
- def handle_error(self, request, client_address):
594
- # HTTP errors must never print archive paths, input or tracebacks.
595
- pass
596
-
597
- with _lock(workspace, ".preview-binding.lock"):
598
- endpoint = _endpoint(workspace, workspace_id)
599
- if endpoint:
600
- token = endpoint["token"]
601
- base = "/p/" + token + "/"
602
- try:
603
- server = Server(("127.0.0.1", endpoint["port"] if endpoint else 0), Handler)
604
- except OSError:
605
- _fail("PREVIEW_PORT_IN_USE", "当前搭子的固定端口无法使用;未切换地址或停止其他程序。")
606
- endpoint = {"version": 1, "workspaceId": workspace_id, "port": server.server_port, "token": token}
607
- try:
608
- _write(workspace / "preview-endpoint.json", endpoint)
609
- _write(workspace / "preview-server.json", {"workspaceId": workspace_id, "pid": os.getpid(),
610
- "url": _url(endpoint), "runtimeVersion": RUNTIME_VERSION})
611
- except Exception:
612
- server.server_close()
613
- raise
614
- try:
615
- server.serve_forever(poll_interval=0.5)
616
- finally:
617
- server.server_close()