mindnet 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {mindnet-0.2.0 → mindnet-0.3.0}/PKG-INFO +1 -1
- mindnet-0.3.0/src/mindnet/__init__.py +5 -0
- {mindnet-0.2.0 → mindnet-0.3.0}/src/mindnet/bot.py +226 -10
- mindnet-0.2.0/src/mindnet/__init__.py +0 -5
- {mindnet-0.2.0 → mindnet-0.3.0}/.gitignore +0 -0
- {mindnet-0.2.0 → mindnet-0.3.0}/README.md +0 -0
- {mindnet-0.2.0 → mindnet-0.3.0}/pyproject.toml +0 -0
- {mindnet-0.2.0 → mindnet-0.3.0}/src/mindnet/__main__.py +0 -0
- {mindnet-0.2.0 → mindnet-0.3.0}/src/mindnet/channel.py +0 -0
- {mindnet-0.2.0 → mindnet-0.3.0}/src/mindnet/cli.py +0 -0
- {mindnet-0.2.0 → mindnet-0.3.0}/src/mindnet/data/SKILL-claude.md +0 -0
- {mindnet-0.2.0 → mindnet-0.3.0}/src/mindnet/data/SKILL-codex.md +0 -0
- {mindnet-0.2.0 → mindnet-0.3.0}/src/mindnet/mcp.py +0 -0
- {mindnet-0.2.0 → mindnet-0.3.0}/src/mindnet/neinao_cli.py +0 -0
- {mindnet-0.2.0 → mindnet-0.3.0}/src/mindnet/skill_install.py +0 -0
- {mindnet-0.2.0 → mindnet-0.3.0}/src/mindnet/update_check.py +0 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"""mindnet —— a reverse knowledge base. The index is you.
|
|
2
|
+
|
|
3
|
+
单入口 CLI(发行面真相源 docs/方案-mindnet-cli合一-v1.md):四件散装收编成 `mindnet` 子命令。
|
|
4
|
+
"""
|
|
5
|
+
__version__ = "0.3.0" # 0.3.0:bridge 多轮 thread(--resume 原生续聊·映射 ~/.mindnet/bot/threads/·丢了退 replay 重建)+ answer_chunk 逐字流式上行(老服务端/老帧向后兼容)
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
两个角色(同一条 WS·/agent/channel):
|
|
5
5
|
· 内脑通道(原 bridge):网页门 /ask 路由到你本地 agent 答(零计费)。
|
|
6
6
|
· **空间 bot(新)**:同空间成员 @ 你的 bot → 云端推问题过来 → 本地 agent 答 → 带署名回对话流。
|
|
7
|
+
多轮 thread(智能体升级 v1·契约 §4):ask 帧带 thread_id → `claude -p --resume` 原生续聊
|
|
8
|
+
(映射落 ~/.mindnet/bot/threads/)+ answer_chunk 逐字流式上行;老服务端/老帧全向后兼容。
|
|
7
9
|
|
|
8
10
|
用法(mindnet 单入口·原 bridge/mindnet_bot.py 收编):
|
|
9
11
|
mindnet login # device-code 网页授权,存 token(顶层收拢·mindnet bot login 同效)
|
|
@@ -35,6 +37,7 @@ import shutil
|
|
|
35
37
|
import subprocess
|
|
36
38
|
import sys
|
|
37
39
|
import tempfile
|
|
40
|
+
import threading
|
|
38
41
|
import time
|
|
39
42
|
import urllib.parse
|
|
40
43
|
import urllib.request
|
|
@@ -84,9 +87,14 @@ def select_exec_mode(capability: str) -> str:
|
|
|
84
87
|
|
|
85
88
|
|
|
86
89
|
# ── 执行路径(claude 后端)──
|
|
87
|
-
def _claude_cmd(prompt: str, system: str, extra: list, model: str | None = None
|
|
90
|
+
def _claude_cmd(prompt: str, system: str, extra: list, model: str | None = None,
|
|
91
|
+
resume: str | None = None, output_format: str = "json") -> list:
|
|
88
92
|
"""拼 claude CLI 命令(纯函数·可单测)。model 显式给了才 --model 透传(真选执行模型·署名诚实底线)。
|
|
89
93
|
|
|
94
|
+
多轮 thread 路(契约 §4):resume=session_id → `--resume` 原生续聊;
|
|
95
|
+
output_format="stream-json" → 逐字流式(成对带 --include-partial-messages --verbose,
|
|
96
|
+
print 模式下 stream-json 必须 --verbose 才吐行)。默认参数下形状与老单发路完全一致。
|
|
97
|
+
|
|
90
98
|
--setting-sources project = 隐私洞修换轨(洞=TATOAO 07-09 真机坐实:claude -p 默认注入主人
|
|
91
99
|
~/.claude/CLAUDE.md 等个人配置,bot 会说给空间成员听)。原修用 --safe-mode,但它把工作区
|
|
92
100
|
.claude/skills/ 也一并剥(勘察档 2026-07-10 实测死碰撞);换「只认项目级设置源」后四项全对:
|
|
@@ -94,17 +102,22 @@ def _claude_cmd(prompt: str, system: str, extra: list, model: str | None = None)
|
|
|
94
102
|
不可用。三级(L0/L1/L2)共用本函数 = 全盖(L0 空 cwd 无项目源,效果等同全剥)。
|
|
95
103
|
注意:邮箱/日期等环境注入 flag 层堵不掉(勘察档遗留①)——靠系统词硬约束兜(件6)。
|
|
96
104
|
"""
|
|
97
|
-
cmd = ["claude", "-p", prompt, "--output-format",
|
|
105
|
+
cmd = ["claude", "-p", prompt, "--output-format", output_format,
|
|
98
106
|
"--system-prompt", system or DEFAULT_SYS,
|
|
99
107
|
"--exclude-dynamic-system-prompt-sections",
|
|
100
108
|
"--setting-sources", "project"]
|
|
109
|
+
if output_format == "stream-json":
|
|
110
|
+
cmd += ["--include-partial-messages", "--verbose"]
|
|
111
|
+
if resume:
|
|
112
|
+
cmd += ["--resume", resume]
|
|
101
113
|
if model:
|
|
102
114
|
cmd += ["--model", model]
|
|
103
115
|
return cmd + list(extra)
|
|
104
116
|
|
|
105
117
|
|
|
106
|
-
def
|
|
107
|
-
|
|
118
|
+
def _claude_json(prompt: str, system: str, cwd: str, extra: list, timeout: float,
|
|
119
|
+
model: str | None = None) -> dict:
|
|
120
|
+
"""单发跑 claude,返完整输出 dict(result 文本 + session_id 等)。失败抛 RuntimeError。"""
|
|
108
121
|
cmd = _claude_cmd(prompt, system, extra, model)
|
|
109
122
|
p = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
|
|
110
123
|
if p.returncode != 0:
|
|
@@ -112,7 +125,12 @@ def _claude(prompt: str, system: str, cwd: str, extra: list, timeout: float,
|
|
|
112
125
|
out = json.loads(p.stdout)
|
|
113
126
|
if out.get("is_error"):
|
|
114
127
|
raise RuntimeError(str(out.get("result"))[:300])
|
|
115
|
-
return
|
|
128
|
+
return out
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _claude(prompt: str, system: str, cwd: str, extra: list, timeout: float,
|
|
132
|
+
model: str | None = None) -> str:
|
|
133
|
+
return str(_claude_json(prompt, system, cwd, extra, timeout, model).get("result") or "").strip()
|
|
116
134
|
|
|
117
135
|
|
|
118
136
|
def _snapshot_workspace(workspace: str, cwd: str) -> None:
|
|
@@ -121,8 +139,16 @@ def _snapshot_workspace(workspace: str, cwd: str) -> None:
|
|
|
121
139
|
|
|
122
140
|
|
|
123
141
|
def _answer_claude(prompt: str, system: str, mode: str, log, timeout: float = 180,
|
|
124
|
-
model: str | None = None, workspace: str | None = None
|
|
125
|
-
|
|
142
|
+
model: str | None = None, workspace: str | None = None,
|
|
143
|
+
thread_id: str | None = None, replay=None, on_chunk=None) -> str:
|
|
144
|
+
"""workspace=空间工作区根(外脑镜像+技能·件4)。L0 不吃 workspace(v1.1 钉·维持测试档)。
|
|
145
|
+
|
|
146
|
+
thread_id 有值=多轮 thread 路(契约 §4:--resume 续聊 + answer_chunk 逐字上行);
|
|
147
|
+
无值=老单发路,行为一字不变(老帧向后兼容)。
|
|
148
|
+
"""
|
|
149
|
+
if thread_id:
|
|
150
|
+
return _answer_claude_thread(prompt, system, mode, log, timeout, model, workspace,
|
|
151
|
+
thread_id, replay, on_chunk)
|
|
126
152
|
if mode == "l0":
|
|
127
153
|
log(" · L0 纯问答(空 cwd·禁全部工具)")
|
|
128
154
|
with tempfile.TemporaryDirectory() as cwd:
|
|
@@ -153,9 +179,16 @@ def _codex_cmd(full: str, sandbox: str, cwd: str, model: str | None = None) -> l
|
|
|
153
179
|
|
|
154
180
|
|
|
155
181
|
def _answer_codex(prompt: str, system: str, mode: str, log, timeout: float = 180,
|
|
156
|
-
model: str | None = None, workspace: str | None = None
|
|
182
|
+
model: str | None = None, workspace: str | None = None,
|
|
183
|
+
thread_id: str | None = None, replay=None, on_chunk=None) -> str:
|
|
157
184
|
"""codex exec 非交互作答。L0=沙箱只读(不吃 workspace·v1.1);L1=workspace-write 快照临时 cwd;
|
|
158
|
-
L2=danger 全权·空间工作区真目录(警示)。工作区说明走 cwd 的 AGENTS.md(codex 原生注入·勘实)。
|
|
185
|
+
L2=danger 全权·空间工作区真目录(警示)。工作区说明走 cwd 的 AGENTS.md(codex 原生注入·勘实)。
|
|
186
|
+
|
|
187
|
+
多轮对称(契约 §4):thread_id 有值 → 每轮 replay 重放重建(codex exec 无稳定 session 续聊面,
|
|
188
|
+
诚实不硬凑映射);整段一次回=服务端当单 chunk(on_chunk 不用),不炸不装流。
|
|
189
|
+
"""
|
|
190
|
+
if thread_id:
|
|
191
|
+
prompt = format_replay(replay, prompt)
|
|
159
192
|
full = (system or DEFAULT_SYS) + "\n\n【问题】\n" + prompt
|
|
160
193
|
if mode == "l0":
|
|
161
194
|
log(" · L0 纯问答(codex·只读沙箱)")
|
|
@@ -180,6 +213,183 @@ def make_answerer(backend: str):
|
|
|
180
213
|
return _answer_codex if backend == "codex" else _answer_claude
|
|
181
214
|
|
|
182
215
|
|
|
216
|
+
# ── 多轮 thread(智能体升级 v1·契约 §4)──
|
|
217
|
+
# ask 帧扩 thread_id + replay(服务端已裁窗);bridge 维护 thread_id→本地 session 映射
|
|
218
|
+
# (状态文件 ~/.mindnet/bot/threads/sessions.json + per-thread 稳定 cwd——claude session
|
|
219
|
+
# 按项目目录归档,cwd 不稳 --resume 找不到)。映射在=--resume 原生续聊(订阅侧免重放,
|
|
220
|
+
# 零计费路再白拿一次);映射丢/resume 报错=弃映射退 replay 重放重建,功能不减。
|
|
221
|
+
|
|
222
|
+
def _bot_threads_dir() -> Path:
|
|
223
|
+
return Path(os.environ.get("MINDNET_BOT_THREADS_DIR",
|
|
224
|
+
str(Path.home() / ".mindnet" / "bot" / "threads")))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _safe_thread_key(thread_id: str) -> str:
|
|
228
|
+
"""thread_id 落盘前消毒(云侧数据不全信·与 _ws_safe_rel 同纪律):只留字母数字与 -_。"""
|
|
229
|
+
key = "".join(c for c in (thread_id or "") if c.isalnum() or c in "-_")
|
|
230
|
+
if not key:
|
|
231
|
+
raise ValueError(f"非法 thread_id:{thread_id!r}")
|
|
232
|
+
return key
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def load_thread_sessions() -> dict:
|
|
236
|
+
p = _bot_threads_dir() / "sessions.json"
|
|
237
|
+
try:
|
|
238
|
+
return json.loads(p.read_text()) if p.exists() else {}
|
|
239
|
+
except Exception:
|
|
240
|
+
return {} # 状态文件坏=当空表:丢的只是 --resume 优化,replay 重建兜底
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def save_thread_session(thread_id: str, entry: dict) -> None:
|
|
244
|
+
d = _bot_threads_dir()
|
|
245
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
246
|
+
s = load_thread_sessions()
|
|
247
|
+
s[thread_id] = entry
|
|
248
|
+
(d / "sessions.json").write_text(json.dumps(s, ensure_ascii=False))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def drop_thread_session(thread_id: str) -> None:
|
|
252
|
+
s = load_thread_sessions()
|
|
253
|
+
if thread_id in s:
|
|
254
|
+
del s[thread_id]
|
|
255
|
+
(_bot_threads_dir() / "sessions.json").write_text(json.dumps(s, ensure_ascii=False))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def thread_cwd(thread_id: str) -> Path:
|
|
259
|
+
p = _bot_threads_dir() / _safe_thread_key(thread_id)
|
|
260
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
261
|
+
return p
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def format_replay(replay, prompt: str) -> str:
|
|
265
|
+
"""映射丢失的重建路(纯函数):把服务端重放的中立历史嵌进单发 prompt。空 replay=原 prompt。"""
|
|
266
|
+
if not replay:
|
|
267
|
+
return prompt
|
|
268
|
+
lines = [("用户" if m.get("role") == "user" else "助手") + ":" + str(m.get("content") or "")
|
|
269
|
+
for m in replay]
|
|
270
|
+
return ("【此前对话(服务端重放·你就是其中的助手,接着聊)】\n" + "\n\n".join(lines)
|
|
271
|
+
+ "\n\n【当前问题】\n" + prompt)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def parse_stream_line(line: str):
|
|
275
|
+
"""claude `--output-format stream-json` 单行 → ("chunk", 增量文本) / ("result", {text,
|
|
276
|
+
session_id, is_error}) / ("other", None)。纯函数·可单测;坏行一律 other 不炸。"""
|
|
277
|
+
try:
|
|
278
|
+
d = json.loads(line)
|
|
279
|
+
except Exception:
|
|
280
|
+
return ("other", None)
|
|
281
|
+
t = d.get("type")
|
|
282
|
+
if t == "stream_event":
|
|
283
|
+
ev = d.get("event") or {}
|
|
284
|
+
if ev.get("type") == "content_block_delta":
|
|
285
|
+
delta = ev.get("delta") or {}
|
|
286
|
+
if delta.get("type") == "text_delta" and delta.get("text"):
|
|
287
|
+
return ("chunk", delta["text"])
|
|
288
|
+
return ("other", None)
|
|
289
|
+
if t == "result":
|
|
290
|
+
return ("result", {"text": str(d.get("result") or "").strip(),
|
|
291
|
+
"session_id": d.get("session_id"),
|
|
292
|
+
"is_error": bool(d.get("is_error"))})
|
|
293
|
+
return ("other", None)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _claude_stream(prompt: str, system: str, cwd: str, extra: list, timeout: float,
|
|
297
|
+
model: str | None = None, resume: str | None = None,
|
|
298
|
+
on_chunk=None) -> tuple:
|
|
299
|
+
"""流式跑 claude(thread 路):stream-json 逐字回调 on_chunk,返 (整段文本, session_id)。
|
|
300
|
+
|
|
301
|
+
· resume 报错**上抛**(调用方弃映射退 replay 重试)——绝不静默退单发,那会丢上下文;
|
|
302
|
+
· 非 resume 且 CLI 接不住 stream-json(旧版无 flag)→ 退老单发 json 整段(诚实单 chunk,
|
|
303
|
+
session_id 也从 json 输出里拿,多轮照样建映射)。
|
|
304
|
+
"""
|
|
305
|
+
cmd = _claude_cmd(prompt, system, extra, model, resume=resume, output_format="stream-json")
|
|
306
|
+
chunks: list = []
|
|
307
|
+
result = None
|
|
308
|
+
p = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
309
|
+
killer = threading.Timer(timeout, p.kill) # 行读没有 run(timeout=) 可用,watchdog 兜死等
|
|
310
|
+
killer.start()
|
|
311
|
+
try:
|
|
312
|
+
for line in p.stdout:
|
|
313
|
+
kind, payload = parse_stream_line(line)
|
|
314
|
+
if kind == "chunk":
|
|
315
|
+
chunks.append(payload)
|
|
316
|
+
if on_chunk:
|
|
317
|
+
try:
|
|
318
|
+
on_chunk(payload)
|
|
319
|
+
except Exception:
|
|
320
|
+
pass # 上行帧失败不打断作答:终帧 answer 仍是完整文本
|
|
321
|
+
elif kind == "result":
|
|
322
|
+
result = payload
|
|
323
|
+
rc = p.wait()
|
|
324
|
+
finally:
|
|
325
|
+
killer.cancel()
|
|
326
|
+
try:
|
|
327
|
+
p.kill()
|
|
328
|
+
except Exception:
|
|
329
|
+
pass
|
|
330
|
+
if result is not None:
|
|
331
|
+
if result["is_error"]:
|
|
332
|
+
raise RuntimeError((result["text"] or "claude 报错")[:300])
|
|
333
|
+
return result["text"] or "".join(chunks), result.get("session_id")
|
|
334
|
+
stderr = (p.stderr.read() or "")[:300] if p.stderr else ""
|
|
335
|
+
if resume:
|
|
336
|
+
raise RuntimeError(stderr or f"claude --resume 失败(退出码 {rc})")
|
|
337
|
+
# 旧 CLI 接不住 stream-json → 单发 json 兜底(整段=answer 终帧一次回,服务端当单 chunk)
|
|
338
|
+
out = _claude_json(prompt, system, cwd, extra, timeout, model)
|
|
339
|
+
text = str(out.get("result") or "").strip()
|
|
340
|
+
if on_chunk and text:
|
|
341
|
+
try:
|
|
342
|
+
on_chunk(text)
|
|
343
|
+
except Exception:
|
|
344
|
+
pass
|
|
345
|
+
return text, out.get("session_id")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _chunk_sender(ws, loop, request_id: str):
|
|
349
|
+
"""answer_chunk 上行帧回调(契约 §4)。answerer 跑在工作线程(asyncio.to_thread),
|
|
350
|
+
经 run_coroutine_threadsafe 投回事件循环发帧;发不出不致命(终帧 answer=完整文本兜底,
|
|
351
|
+
老服务端收到未知帧忽略不炸=向后兼容)。"""
|
|
352
|
+
def cb(text: str) -> None:
|
|
353
|
+
if not text:
|
|
354
|
+
return
|
|
355
|
+
frame = json.dumps({"type": "answer_chunk", "request_id": request_id, "text": text})
|
|
356
|
+
try:
|
|
357
|
+
asyncio.run_coroutine_threadsafe(ws.send(frame), loop)
|
|
358
|
+
except Exception:
|
|
359
|
+
pass
|
|
360
|
+
return cb
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _answer_claude_thread(prompt: str, system: str, mode: str, log, timeout: float,
|
|
364
|
+
model: str | None, workspace: str | None,
|
|
365
|
+
thread_id: str, replay, on_chunk) -> str:
|
|
366
|
+
"""多轮 thread 的 claude 路:稳定 per-thread cwd + 映射 --resume;丢了退 replay 重建。"""
|
|
367
|
+
extra = (["--disallowed-tools", *_ALL_TOOLS] if mode == "l0"
|
|
368
|
+
else ["--disallowed-tools", *_L1_DISALLOW] if mode == "l1_restricted" else [])
|
|
369
|
+
cwd = Path(workspace) if (mode == "l2" and workspace) else thread_cwd(thread_id)
|
|
370
|
+
if workspace and mode == "l1_restricted":
|
|
371
|
+
_snapshot_workspace(workspace, str(cwd))
|
|
372
|
+
sess = load_thread_sessions().get(thread_id) or {}
|
|
373
|
+
sid = sess.get("session_id") if sess.get("backend", "claude") == "claude" else None
|
|
374
|
+
if sid:
|
|
375
|
+
try:
|
|
376
|
+
log(f" · thread {thread_id[:16]} --resume 原生续聊")
|
|
377
|
+
text, new_sid = _claude_stream(prompt, system, str(sess.get("cwd") or cwd), extra,
|
|
378
|
+
timeout, model, resume=sid, on_chunk=on_chunk)
|
|
379
|
+
save_thread_session(thread_id, {"session_id": new_sid or sid,
|
|
380
|
+
"backend": "claude", "cwd": str(cwd)})
|
|
381
|
+
return text
|
|
382
|
+
except Exception as e:
|
|
383
|
+
log(f" ⚠ resume 失败({str(e)[:80]}),弃映射退重放重建")
|
|
384
|
+
drop_thread_session(thread_id)
|
|
385
|
+
text, new_sid = _claude_stream(format_replay(replay, prompt), system, str(cwd), extra,
|
|
386
|
+
timeout, model, on_chunk=on_chunk)
|
|
387
|
+
if new_sid:
|
|
388
|
+
save_thread_session(thread_id, {"session_id": new_sid, "backend": "claude",
|
|
389
|
+
"cwd": str(cwd)})
|
|
390
|
+
return text
|
|
391
|
+
|
|
392
|
+
|
|
183
393
|
# ── bot 工作区(件3 云→本地拉·manifest 下行)──
|
|
184
394
|
# 工作区布局(方向档 §一):~/.mindnet/bot/spaces/<space_id>/{外脑/, .claude/skills/, .pull-manifest.json}
|
|
185
395
|
BRAIN_DIR = "外脑"
|
|
@@ -429,8 +639,14 @@ async def run(exposed_caps: dict, backend: str, model: str, once: bool = False,
|
|
|
429
639
|
workspace = None
|
|
430
640
|
if space_id and mode != "l0": # L0 不吃工作区(v1.1 钉·测试档)
|
|
431
641
|
workspace = await asyncio.to_thread(_workspace_for, space_id, token, log)
|
|
642
|
+
# 多轮 thread(契约 §4·仅有值才走):--resume 续聊 + answer_chunk 逐字上行
|
|
643
|
+
thread_id = msg.get("thread_id")
|
|
644
|
+
on_chunk = (_chunk_sender(ws, asyncio.get_running_loop(), rid)
|
|
645
|
+
if thread_id else None)
|
|
432
646
|
text = await asyncio.to_thread(answerer, msg.get("prompt", ""), system,
|
|
433
|
-
mode, log, model=exec_model, workspace=workspace
|
|
647
|
+
mode, log, model=exec_model, workspace=workspace,
|
|
648
|
+
thread_id=thread_id, replay=msg.get("replay"),
|
|
649
|
+
on_chunk=on_chunk)
|
|
434
650
|
await ws.send(json.dumps({"type": "answer", "request_id": rid, "ok": True, "text": text}))
|
|
435
651
|
log(f" ✓ 答了({len(text)} 字)")
|
|
436
652
|
except Exception as e:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|