@my-life-buddies/cli 0.1.2

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 (158) hide show
  1. package/README.md +314 -0
  2. package/dist/bin/buddy.js +443 -0
  3. package/dist/bin/buddy.js.map +7 -0
  4. package/dist/bin/core.js +5638 -0
  5. package/dist/bin/core.js.map +7 -0
  6. package/dist/bin/preview.js +1429 -0
  7. package/dist/bin/preview.js.map +7 -0
  8. package/dist/web/app.css +508 -0
  9. package/dist/web/app.js +724 -0
  10. package/dist/web/assets/developer-platform-icon.png.base64 +1 -0
  11. package/dist/web/brand.css +100 -0
  12. package/dist/web/index.html +236 -0
  13. package/dist/web/tokens.css +15 -0
  14. package/package.json +45 -0
  15. package/resources/README.md +52 -0
  16. package/resources/agent-template/package-lock.json +1726 -0
  17. package/resources/agent-template/package.json +16 -0
  18. package/resources/brand/README.md +11 -0
  19. package/resources/brand/login-result.css +25 -0
  20. package/resources/brand/logo.png +0 -0
  21. package/resources/brand/tokens.css +100 -0
  22. package/resources/buddy-creator/INSTALL.md +25 -0
  23. package/resources/buddy-creator/PATCHES.md +33 -0
  24. package/resources/buddy-creator/SKILL.md +66 -0
  25. package/resources/buddy-creator/THIRD_PARTY_NOTICES.md +154 -0
  26. package/resources/buddy-creator/agents/openai.yaml +4 -0
  27. package/resources/buddy-creator/assets/licenses/bail.txt +26 -0
  28. package/resources/buddy-creator/assets/licenses/ccount.txt +26 -0
  29. package/resources/buddy-creator/assets/licenses/character-entities-html4.txt +26 -0
  30. package/resources/buddy-creator/assets/licenses/character-entities-legacy.txt +26 -0
  31. package/resources/buddy-creator/assets/licenses/character-entities.txt +26 -0
  32. package/resources/buddy-creator/assets/licenses/character-reference-invalid.txt +26 -0
  33. package/resources/buddy-creator/assets/licenses/comma-separated-tokens.txt +26 -0
  34. package/resources/buddy-creator/assets/licenses/debug.txt +24 -0
  35. package/resources/buddy-creator/assets/licenses/decode-named-character-reference.txt +26 -0
  36. package/resources/buddy-creator/assets/licenses/dequal.txt +25 -0
  37. package/resources/buddy-creator/assets/licenses/devlop.txt +26 -0
  38. package/resources/buddy-creator/assets/licenses/escape-string-regexp.txt +13 -0
  39. package/resources/buddy-creator/assets/licenses/estree-util-is-identifier-name.txt +26 -0
  40. package/resources/buddy-creator/assets/licenses/extend.txt +27 -0
  41. package/resources/buddy-creator/assets/licenses/hast-util-to-jsx-runtime.txt +26 -0
  42. package/resources/buddy-creator/assets/licenses/hast-util-whitespace.txt +26 -0
  43. package/resources/buddy-creator/assets/licenses/html-url-attributes.txt +25 -0
  44. package/resources/buddy-creator/assets/licenses/inline-style-parser.txt +13 -0
  45. package/resources/buddy-creator/assets/licenses/is-alphabetical.txt +26 -0
  46. package/resources/buddy-creator/assets/licenses/is-alphanumerical.txt +26 -0
  47. package/resources/buddy-creator/assets/licenses/is-decimal.txt +26 -0
  48. package/resources/buddy-creator/assets/licenses/is-hexadecimal.txt +26 -0
  49. package/resources/buddy-creator/assets/licenses/is-plain-obj.txt +13 -0
  50. package/resources/buddy-creator/assets/licenses/longest-streak.txt +26 -0
  51. package/resources/buddy-creator/assets/licenses/markdown-table.txt +26 -0
  52. package/resources/buddy-creator/assets/licenses/mdast-util-find-and-replace.txt +26 -0
  53. package/resources/buddy-creator/assets/licenses/mdast-util-from-markdown.txt +26 -0
  54. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-autolink-literal.txt +26 -0
  55. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-footnote.txt +26 -0
  56. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-strikethrough.txt +26 -0
  57. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-table.txt +26 -0
  58. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-task-list-item.txt +26 -0
  59. package/resources/buddy-creator/assets/licenses/mdast-util-gfm.txt +26 -0
  60. package/resources/buddy-creator/assets/licenses/mdast-util-mdx-expression.txt +26 -0
  61. package/resources/buddy-creator/assets/licenses/mdast-util-mdx-jsx.txt +26 -0
  62. package/resources/buddy-creator/assets/licenses/mdast-util-mdxjs-esm.txt +26 -0
  63. package/resources/buddy-creator/assets/licenses/mdast-util-phrasing.txt +27 -0
  64. package/resources/buddy-creator/assets/licenses/mdast-util-to-hast.txt +26 -0
  65. package/resources/buddy-creator/assets/licenses/mdast-util-to-markdown.txt +26 -0
  66. package/resources/buddy-creator/assets/licenses/mdast-util-to-string.txt +26 -0
  67. package/resources/buddy-creator/assets/licenses/micromark-core-commonmark.txt +26 -0
  68. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-autolink-literal.txt +26 -0
  69. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-footnote.txt +26 -0
  70. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-strikethrough.txt +26 -0
  71. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-table.txt +26 -0
  72. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-tagfilter.txt +26 -0
  73. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-task-list-item.txt +26 -0
  74. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm.txt +26 -0
  75. package/resources/buddy-creator/assets/licenses/micromark-factory-destination.txt +26 -0
  76. package/resources/buddy-creator/assets/licenses/micromark-factory-label.txt +26 -0
  77. package/resources/buddy-creator/assets/licenses/micromark-factory-space.txt +26 -0
  78. package/resources/buddy-creator/assets/licenses/micromark-factory-title.txt +26 -0
  79. package/resources/buddy-creator/assets/licenses/micromark-factory-whitespace.txt +26 -0
  80. package/resources/buddy-creator/assets/licenses/micromark-util-character.txt +26 -0
  81. package/resources/buddy-creator/assets/licenses/micromark-util-chunked.txt +26 -0
  82. package/resources/buddy-creator/assets/licenses/micromark-util-classify-character.txt +26 -0
  83. package/resources/buddy-creator/assets/licenses/micromark-util-combine-extensions.txt +26 -0
  84. package/resources/buddy-creator/assets/licenses/micromark-util-decode-numeric-character-reference.txt +26 -0
  85. package/resources/buddy-creator/assets/licenses/micromark-util-decode-string.txt +26 -0
  86. package/resources/buddy-creator/assets/licenses/micromark-util-encode.txt +26 -0
  87. package/resources/buddy-creator/assets/licenses/micromark-util-html-tag-name.txt +26 -0
  88. package/resources/buddy-creator/assets/licenses/micromark-util-normalize-identifier.txt +26 -0
  89. package/resources/buddy-creator/assets/licenses/micromark-util-resolve-all.txt +26 -0
  90. package/resources/buddy-creator/assets/licenses/micromark-util-sanitize-uri.txt +26 -0
  91. package/resources/buddy-creator/assets/licenses/micromark-util-subtokenize.txt +26 -0
  92. package/resources/buddy-creator/assets/licenses/micromark-util-symbol.txt +26 -0
  93. package/resources/buddy-creator/assets/licenses/micromark-util-types.txt +26 -0
  94. package/resources/buddy-creator/assets/licenses/micromark.txt +26 -0
  95. package/resources/buddy-creator/assets/licenses/ms.txt +25 -0
  96. package/resources/buddy-creator/assets/licenses/parse-entities.txt +26 -0
  97. package/resources/buddy-creator/assets/licenses/phosphor-icons--react.txt +25 -0
  98. package/resources/buddy-creator/assets/licenses/property-information.txt +26 -0
  99. package/resources/buddy-creator/assets/licenses/react-dom.txt +25 -0
  100. package/resources/buddy-creator/assets/licenses/react-markdown.txt +25 -0
  101. package/resources/buddy-creator/assets/licenses/react.txt +25 -0
  102. package/resources/buddy-creator/assets/licenses/remark-gfm.txt +26 -0
  103. package/resources/buddy-creator/assets/licenses/remark-parse.txt +25 -0
  104. package/resources/buddy-creator/assets/licenses/remark-rehype.txt +26 -0
  105. package/resources/buddy-creator/assets/licenses/remark-stringify.txt +25 -0
  106. package/resources/buddy-creator/assets/licenses/scheduler.txt +25 -0
  107. package/resources/buddy-creator/assets/licenses/space-separated-tokens.txt +26 -0
  108. package/resources/buddy-creator/assets/licenses/stringify-entities.txt +26 -0
  109. package/resources/buddy-creator/assets/licenses/style-to-js.txt +26 -0
  110. package/resources/buddy-creator/assets/licenses/style-to-object.txt +26 -0
  111. package/resources/buddy-creator/assets/licenses/trim-lines.txt +26 -0
  112. package/resources/buddy-creator/assets/licenses/trough.txt +25 -0
  113. package/resources/buddy-creator/assets/licenses/types--debug.txt +25 -0
  114. package/resources/buddy-creator/assets/licenses/types--estree-jsx.txt +25 -0
  115. package/resources/buddy-creator/assets/licenses/types--estree.txt +25 -0
  116. package/resources/buddy-creator/assets/licenses/types--hast.txt +25 -0
  117. package/resources/buddy-creator/assets/licenses/types--mdast.txt +25 -0
  118. package/resources/buddy-creator/assets/licenses/types--ms.txt +25 -0
  119. package/resources/buddy-creator/assets/licenses/types--unist.txt +25 -0
  120. package/resources/buddy-creator/assets/licenses/ungap--structured-clone.txt +19 -0
  121. package/resources/buddy-creator/assets/licenses/unified.txt +25 -0
  122. package/resources/buddy-creator/assets/licenses/unist-util-is.txt +26 -0
  123. package/resources/buddy-creator/assets/licenses/unist-util-position.txt +26 -0
  124. package/resources/buddy-creator/assets/licenses/unist-util-stringify-position.txt +26 -0
  125. package/resources/buddy-creator/assets/licenses/unist-util-visit-parents.txt +26 -0
  126. package/resources/buddy-creator/assets/licenses/unist-util-visit.txt +26 -0
  127. package/resources/buddy-creator/assets/licenses/vfile-message.txt +26 -0
  128. package/resources/buddy-creator/assets/licenses/vfile.txt +25 -0
  129. package/resources/buddy-creator/assets/licenses/zwitch.txt +26 -0
  130. package/resources/buddy-creator/assets/preview/assets/index-B5VmkoSx.js +39 -0
  131. package/resources/buddy-creator/assets/preview/assets/index-DWKP6b-3.css +4 -0
  132. package/resources/buddy-creator/assets/preview/brand-logo.png +0 -0
  133. package/resources/buddy-creator/assets/preview/brand.css +100 -0
  134. package/resources/buddy-creator/assets/preview/index.html +5 -0
  135. package/resources/buddy-creator/assets/preview/reader.css +165 -0
  136. package/resources/buddy-creator/assets/preview/reader.js +135 -0
  137. package/resources/buddy-creator/references/artifact-schema.md +68 -0
  138. package/resources/buddy-creator/references/catalog.json +401 -0
  139. package/resources/buddy-creator/references/definition.md +24 -0
  140. package/resources/buddy-creator/references/dialogue-examples.md +133 -0
  141. package/resources/buddy-creator/references/host-guide.md +213 -0
  142. package/resources/buddy-creator/references/interview.md +235 -0
  143. package/resources/buddy-creator/references/knowledge.md +40 -0
  144. package/resources/buddy-creator/references/methods.md +77 -0
  145. package/resources/buddy-creator/references/opening.md +14 -0
  146. package/resources/buddy-creator/references/preview-panel.md +66 -0
  147. package/resources/buddy-creator/references/recovery.md +43 -0
  148. package/resources/buddy-creator/references/service.md +74 -0
  149. package/resources/buddy-creator/scripts/buddy.py +108 -0
  150. package/resources/buddy-creator/scripts/buddy_client.py +67 -0
  151. package/resources/buddy-creator/scripts/buddy_core.py +803 -0
  152. package/resources/buddy-creator/scripts/completion.py +240 -0
  153. package/resources/buddy-creator/scripts/interview.py +215 -0
  154. package/resources/buddy-creator/scripts/preview.py +577 -0
  155. package/resources/buddy-creator/scripts/preview_panel.py +80 -0
  156. package/resources/buddy-creator/scripts/sources.py +239 -0
  157. package/resources/buddy-creator/version.json +8 -0
  158. package/resources/buddy-creator.manifest.json +696 -0
@@ -0,0 +1,239 @@
1
+ """Archive original sources and actual host-tool extracts; no document converters."""
2
+ import hashlib
3
+ import json
4
+ import os
5
+ import shutil
6
+ import tempfile
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from urllib.parse import urlsplit
10
+
11
+ KINDS = {'file', 'webpage', 'history', 'mindmap', 'skill', 'oral', 'scan', 'audio', 'video'}
12
+ TEXT_EXTENSIONS = {'.txt', '.md', '.markdown', '.csv', '.tsv', '.json', '.jsonl', '.yaml', '.yml', '.xml', '.opml', '.mm', '.html', '.htm', '.log'}
13
+ MAX_BYTES = 512 * 1024 * 1024
14
+ MAX_TEXT = 32 * 1024 * 1024
15
+
16
+
17
+ def digest(value):
18
+ if isinstance(value, str):
19
+ value = value.encode('utf-8')
20
+ return hashlib.sha256(value).hexdigest()
21
+
22
+
23
+ def encoded(value):
24
+ return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(',', ':'), allow_nan=False).encode('utf-8')
25
+
26
+
27
+ def fail(code, message):
28
+ raise ValueError(code + ': ' + message)
29
+
30
+
31
+ def _part(text, locator):
32
+ if not isinstance(text, str) or not text.strip() or not isinstance(locator, str) or not locator.strip():
33
+ fail('SOURCE_PART', '每段提取结果需要真实原文和来源位置。')
34
+ if len(text.encode('utf-8')) > MAX_TEXT:
35
+ fail('SOURCE_PART_SIZE', '单段资料过大,请按实际来源分段。')
36
+ return {'text': text, 'locator': locator}
37
+
38
+
39
+ def _text(path):
40
+ if path.stat().st_size > MAX_TEXT:
41
+ fail('SOURCE_TEXT_SIZE', '文本过大,请由宿主分段读取后提交 hostResult。')
42
+ try:
43
+ result = path.read_text(encoding='utf-8-sig')
44
+ except UnicodeError:
45
+ fail('HOST_SOURCE_REQUIRED', '资料不是 UTF-8 文本,请由宿主读取后提交 hostResult。')
46
+ if '\x00' in result:
47
+ fail('HOST_SOURCE_REQUIRED', '这份资料需要宿主工具读取,不能当作普通文本。')
48
+ return result
49
+
50
+
51
+ def _history(text):
52
+ try:
53
+ data = json.loads(text)
54
+ rows = data.get('messages', []) if isinstance(data, dict) else data
55
+ except json.JSONDecodeError:
56
+ try:
57
+ rows = [json.loads(line) for line in text.splitlines() if line.strip()]
58
+ except json.JSONDecodeError:
59
+ fail('HISTORY_FORMAT', '历史资料须为用户选定的可见消息 JSON/JSONL,或由宿主提取可见原文。')
60
+ if not isinstance(rows, list):
61
+ fail('HISTORY_FORMAT', '没有找到选定的可见消息列表。')
62
+ parts = []
63
+ for index, row in enumerate(rows):
64
+ if not isinstance(row, dict) or row.get('role') not in ('user', 'assistant'):
65
+ continue
66
+ if row.get('channel') not in (None, 'final') or row.get('hidden') or row.get('visibility') == 'hidden':
67
+ continue
68
+ content = row.get('content', row.get('text', ''))
69
+ if isinstance(content, list):
70
+ content = '\n'.join(item.get('text', '') for item in content if isinstance(item, dict) and item.get('type') in ('text', 'input_text', 'output_text'))
71
+ if isinstance(content, str) and content.strip():
72
+ parts.append(_part(content, '选定可见消息 %s / %s' % (index + 1, row['role'])))
73
+ if not parts:
74
+ fail('HISTORY_EMPTY', '没有可导入的用户或助手可见正文。')
75
+ return parts
76
+
77
+
78
+ def _copy_file(path, destination):
79
+ if not path.is_file() or path.is_symlink():
80
+ fail('SOURCE_FILE', '请提供实际文件,不能用符号链接替代原件。')
81
+ size = path.stat().st_size
82
+ if size > MAX_BYTES:
83
+ fail('SOURCE_SIZE', '单份原件超过 512 MiB 归档上限,请选定必要部分。')
84
+ destination.parent.mkdir(parents=True, exist_ok=True)
85
+ total = 0
86
+ sha = hashlib.sha256()
87
+ with path.open('rb') as incoming, destination.open('xb') as outgoing:
88
+ while True:
89
+ block = incoming.read(1024 * 1024)
90
+ if not block:
91
+ break
92
+ total += len(block)
93
+ if total > MAX_BYTES:
94
+ fail('SOURCE_SIZE', '读取过程中资料超过大小上限。')
95
+ sha.update(block)
96
+ outgoing.write(block)
97
+ outgoing.flush()
98
+ os.fsync(outgoing.fileno())
99
+ return {'hash': sha.hexdigest(), 'bytes': total}
100
+
101
+
102
+ def import_source(workspace, state, payload):
103
+ """Called under the workspace writer lock. Returns an immutable manifest."""
104
+ workspace = Path(workspace)
105
+ op_id = payload.get('operationId')
106
+ if not isinstance(op_id, str) or not op_id.strip() or len(op_id) > 256:
107
+ fail('OPERATION_ID_REQUIRED', '资料导入需要稳定的 operationId;重试沿用,补充结果使用新身份。')
108
+ kind = payload.get('kind')
109
+ if kind not in KINDS:
110
+ fail('SOURCE_UNSUPPORTED', '不支持这类来源;此包不包含小红书账号采集。')
111
+ uri = payload.get('uri', '')
112
+ if not isinstance(uri, str):
113
+ fail('SOURCE_URI', '资料位置必须是字符串。')
114
+ if kind == 'webpage':
115
+ url = urlsplit(uri)
116
+ host = (url.hostname or '').lower().rstrip('.')
117
+ if url.scheme not in ('http', 'https') or not host or url.username or url.password:
118
+ fail('SOURCE_URL', '网页来源需要不含账号密码的 HTTP(S) 地址。')
119
+ if any(host == domain or host.endswith('.' + domain) for domain in ('xiaohongshu.com', 'xhslink.com')):
120
+ fail('SOURCE_UNSUPPORTED', '此包不采集小红书网页或账号;用户自行提供的原文可作为本地资料。')
121
+ request_hash = digest(encoded({key: value for key, value in payload.items() if key not in ('operation', 'operationId')}))
122
+ source_id = 'source_' + digest(op_id)[:24]
123
+ directory = workspace / 'sources' / source_id
124
+ manifest_path = directory / 'manifest.json'
125
+ if manifest_path.exists():
126
+ previous = json.loads(manifest_path.read_text(encoding='utf-8'))
127
+ if previous.get('requestHash') != request_hash:
128
+ fail('IDEMPOTENCY_CONFLICT', '同一资料导入身份对应的内容已改变,请为新结果使用新的 operationId。')
129
+ return previous
130
+ host_result = payload.get('hostResult')
131
+ parts, extraction = [], None
132
+ if host_result is not None:
133
+ if not isinstance(host_result, dict) or not isinstance(host_result.get('tool'), str) or not host_result['tool'].strip():
134
+ fail('HOST_SOURCE_TOOL', '请注明实际读取资料的宿主工具。')
135
+ if host_result.get('coverage') not in ('complete', 'partial'):
136
+ fail('HOST_SOURCE_COVERAGE', 'coverage 必须如实填写 complete 或 partial。')
137
+ rows = host_result.get('parts')
138
+ if not isinstance(rows, list) or not rows or len(rows) > 10000:
139
+ fail('HOST_SOURCE_PARTS', '请提供真实提取的正文和定位。')
140
+ for row in rows:
141
+ if not isinstance(row, dict):
142
+ fail('HOST_SOURCE_PARTS', '每段资料必须是 text 和 locator 组成的对象。')
143
+ parts.append(_part(row.get('text'), row.get('locator')))
144
+ notes = host_result.get('notes', [])
145
+ if not isinstance(notes, list) or any(not isinstance(note, str) for note in notes):
146
+ fail('HOST_SOURCE_NOTES', 'notes 须为实际处理说明的文本列表。')
147
+ extraction = {'provider': 'host', 'tool': host_result['tool'], 'coverage': host_result['coverage'], 'notes': notes}
148
+ elif kind in ('webpage', 'scan', 'audio', 'video'):
149
+ fail('HOST_SOURCE_REQUIRED', '请先用宿主实际可用工具读取资料,再提交 hostResult。')
150
+ source_root = workspace / 'sources'
151
+ source_root.mkdir(parents=True, exist_ok=True)
152
+ staging = Path(tempfile.mkdtemp(prefix='.incoming-', dir=str(source_root)))
153
+ files = []
154
+ try:
155
+ if kind == 'webpage':
156
+ raw = encoded({'url': uri, 'hostResult': host_result, 'description': '宿主提取快照,不是完整网页原件'})
157
+ (staging / 'webpage-snapshot.json').write_bytes(raw)
158
+ files.append({'path': 'webpage-snapshot.json', 'hash': digest(raw), 'bytes': len(raw)})
159
+ elif uri:
160
+ path = Path(uri).expanduser()
161
+ if not path.is_absolute():
162
+ fail('SOURCE_ABSOLUTE_PATH', '本地资料请使用绝对路径。')
163
+ if path.is_symlink():
164
+ fail('SOURCE_FILE', '请提供实际原件路径。')
165
+ if path.is_dir() and kind == 'skill':
166
+ total, count = 0, 0
167
+ for folder, dirs, names in os.walk(str(path), followlinks=False):
168
+ dirs[:] = [name for name in sorted(dirs) if name not in ('.git', 'node_modules', '__pycache__', '.venv') and not (Path(folder) / name).is_symlink()]
169
+ for name in sorted(names):
170
+ item = Path(folder) / name
171
+ if item.is_symlink() or item.suffix.lower() not in TEXT_EXTENSIONS:
172
+ continue
173
+ count += 1
174
+ total += item.stat().st_size
175
+ if count > 5000 or total > MAX_BYTES:
176
+ fail('SOURCE_SIZE', 'Skill 资料超出文本归档上限,请选定相关文档。')
177
+ relative = item.relative_to(path).as_posix()
178
+ saved = _copy_file(item, staging / 'original' / relative)
179
+ files.append(dict(saved, path='original/' + relative))
180
+ if host_result is None:
181
+ text = _text(staging / 'original' / relative)
182
+ if text.strip():
183
+ parts.append(_part(text, relative))
184
+ elif path.is_file():
185
+ saved = _copy_file(path, staging / 'original' / path.name)
186
+ files.append(dict(saved, path='original/' + path.name))
187
+ if host_result is None:
188
+ if path.suffix.lower() not in TEXT_EXTENSIONS:
189
+ fail('HOST_SOURCE_REQUIRED', '此格式请先由宿主工具读取,再提交 hostResult。')
190
+ text = _text(staging / 'original' / path.name)
191
+ parts = _history(text) if kind == 'history' else [_part(text, path.name)]
192
+ else:
193
+ fail('SOURCE_FILE', '资料文件不存在,或该来源类型不支持目录。')
194
+ elif payload.get('text') is not None:
195
+ text = payload['text']
196
+ part = _part(text, '创作者提供的原文' if kind == 'oral' else '用户选定原文')
197
+ raw = text.encode('utf-8')
198
+ (staging / 'original.txt').write_bytes(raw)
199
+ files.append({'path': 'original.txt', 'hash': digest(raw), 'bytes': len(raw)})
200
+ if host_result is None:
201
+ parts = _history(text) if kind == 'history' else [part]
202
+ else:
203
+ fail('SOURCE_ORIGINAL_REQUIRED', '请提供本地原件路径、网页地址或用户实际提供的原文。')
204
+ if not parts:
205
+ fail('SOURCE_EMPTY', '资料中没有可归档的可读正文。')
206
+ if sum(len(part['text'].encode('utf-8')) for part in parts) > MAX_BYTES:
207
+ fail('SOURCE_SIZE', '提取结果总量超过 512 MiB,请按实际范围分批。')
208
+ if host_result is not None:
209
+ raw = encoded(host_result)
210
+ (staging / 'host-result.json').write_bytes(raw)
211
+ files.append({'path': 'host-result.json', 'hash': digest(raw), 'bytes': len(raw)})
212
+ chunks = [dict(part, id='chunk_%04d' % (index + 1), hash=digest(part['text'])) for index, part in enumerate(parts)]
213
+ complete = extraction is None or extraction['coverage'] == 'complete'
214
+ title = payload.get('title') or (Path(uri).name if uri and kind != 'webpage' else uri or '口述资料')
215
+ manifest = {'id': source_id, 'version': digest(encoded({'files': files, 'chunks': chunks})),
216
+ 'operationId': op_id, 'requestHash': request_hash, 'kind': kind, 'title': str(title), 'uri': uri,
217
+ 'status': 'ready' if complete else 'failed', 'files': files, 'chunks': chunks,
218
+ 'extraction': extraction, 'warnings': [] if complete else ['仅保存已提取部分,尚未完整读取。'],
219
+ 'parser': 'host-tools' if extraction else 'python-text-archive',
220
+ 'acquiredAt': datetime.now(timezone.utc).isoformat(),
221
+ 'error': None if complete else 'HOST_SOURCE_INCOMPLETE',
222
+ 'processing': {'acquisition': 'complete' if complete else 'partial',
223
+ 'parsing': 'complete' if complete else 'partial', 'semanticReview': 'requires_host_calibration'}}
224
+ (staging / 'manifest.json').write_bytes(encoded(manifest))
225
+ for file in staging.rglob('*'):
226
+ if file.is_file():
227
+ with file.open('rb') as handle:
228
+ os.fsync(handle.fileno())
229
+ os.replace(str(staging), str(directory))
230
+ if os.name != 'nt':
231
+ fd = os.open(str(source_root), os.O_RDONLY)
232
+ try:
233
+ os.fsync(fd)
234
+ finally:
235
+ os.close(fd)
236
+ return manifest
237
+ except BaseException:
238
+ shutil.rmtree(str(staging), ignore_errors=True)
239
+ raise
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "buddy-creator",
3
+ "displayName": "Buddy Creator Skill",
4
+ "version": "1.3.1",
5
+ "python": ">=3.9",
6
+ "workspaceFormat": "buddy-creator-1",
7
+ "nodeRequired": false
8
+ }