yee88 0.6.3__py3-none-any.whl → 0.7.1__py3-none-any.whl
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.
- yee88/cli/__init__.py +2 -0
- yee88/cli/handoff.py +304 -0
- yee88/cron/manager.py +54 -17
- yee88/ids.py +1 -1
- yee88/skills/yee88/SKILL.md +57 -6
- yee88/telegram/commands/handlers.py +2 -0
- yee88/telegram/commands/menu.py +4 -1
- yee88/telegram/commands/topics.py +82 -0
- yee88/telegram/loop.py +12 -1
- {yee88-0.6.3.dist-info → yee88-0.7.1.dist-info}/METADATA +1 -1
- {yee88-0.6.3.dist-info → yee88-0.7.1.dist-info}/RECORD +14 -13
- {yee88-0.6.3.dist-info → yee88-0.7.1.dist-info}/WHEEL +0 -0
- {yee88-0.6.3.dist-info → yee88-0.7.1.dist-info}/entry_points.txt +0 -0
- {yee88-0.6.3.dist-info → yee88-0.7.1.dist-info}/licenses/LICENSE +0 -0
yee88/cli/__init__.py
CHANGED
|
@@ -92,6 +92,7 @@ from .config import (
|
|
|
92
92
|
config_unset,
|
|
93
93
|
)
|
|
94
94
|
from .cron import app as cron_app
|
|
95
|
+
from .handoff import app as handoff_app
|
|
95
96
|
from .reload import reload_command
|
|
96
97
|
|
|
97
98
|
|
|
@@ -215,6 +216,7 @@ def create_app() -> typer.Typer:
|
|
|
215
216
|
app.command(name="plugins")(plugins_cmd)
|
|
216
217
|
app.add_typer(config_app, name="config")
|
|
217
218
|
app.add_typer(cron_app, name="cron")
|
|
219
|
+
app.add_typer(handoff_app, name="handoff")
|
|
218
220
|
app.command(name="reload")(reload_command)
|
|
219
221
|
app.callback()(app_main)
|
|
220
222
|
for engine_id in _engine_ids_for_cli():
|
yee88/cli/handoff.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import anyio
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from ..context import RunContext
|
|
13
|
+
from ..model import ResumeToken
|
|
14
|
+
from ..settings import load_settings_if_exists
|
|
15
|
+
from ..telegram.client import TelegramClient
|
|
16
|
+
from ..telegram.topic_state import TopicStateStore, resolve_state_path
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(help="Handoff session context to Telegram")
|
|
19
|
+
|
|
20
|
+
OPENCODE_STORAGE = Path.home() / ".local" / "share" / "opencode" / "storage"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class SessionInfo:
|
|
25
|
+
id: str
|
|
26
|
+
directory: str
|
|
27
|
+
updated: float
|
|
28
|
+
title: str
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def project_name(self) -> str:
|
|
32
|
+
return Path(self.directory).name if self.directory else "unknown"
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def updated_str(self) -> str:
|
|
36
|
+
return datetime.fromtimestamp(self.updated / 1000).strftime("%m-%d %H:%M")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _get_recent_sessions(limit: int = 10) -> list[SessionInfo]:
|
|
40
|
+
try:
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
["opencode", "session", "list", "--format", "json", "-n", str(limit)],
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
check=True,
|
|
46
|
+
)
|
|
47
|
+
data = json.loads(result.stdout)
|
|
48
|
+
return [
|
|
49
|
+
SessionInfo(
|
|
50
|
+
id=s.get("id", ""),
|
|
51
|
+
directory=s.get("directory", ""),
|
|
52
|
+
updated=s.get("updated", 0),
|
|
53
|
+
title=s.get("title", ""),
|
|
54
|
+
)
|
|
55
|
+
for s in data
|
|
56
|
+
]
|
|
57
|
+
except (subprocess.CalledProcessError, json.JSONDecodeError, FileNotFoundError):
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _get_session_messages(session_id: str, limit: int = 5) -> list[dict]:
|
|
62
|
+
message_dir = OPENCODE_STORAGE / "message" / session_id
|
|
63
|
+
if not message_dir.exists():
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
messages: list[tuple[int, str, str]] = []
|
|
67
|
+
for msg_file in message_dir.glob("msg_*.json"):
|
|
68
|
+
try:
|
|
69
|
+
data = json.loads(msg_file.read_text())
|
|
70
|
+
created = data.get("time", {}).get("created", 0)
|
|
71
|
+
role = data.get("role", "unknown")
|
|
72
|
+
msg_id = data.get("id", "")
|
|
73
|
+
messages.append((created, role, msg_id))
|
|
74
|
+
except (json.JSONDecodeError, OSError):
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
messages.sort(key=lambda x: x[0], reverse=True)
|
|
78
|
+
messages = messages[:limit]
|
|
79
|
+
messages.reverse()
|
|
80
|
+
|
|
81
|
+
result = []
|
|
82
|
+
for _, role, msg_id in messages:
|
|
83
|
+
part_dir = OPENCODE_STORAGE / "part" / msg_id
|
|
84
|
+
if not part_dir.exists():
|
|
85
|
+
continue
|
|
86
|
+
for part_file in part_dir.glob("prt_*.json"):
|
|
87
|
+
try:
|
|
88
|
+
part_data = json.loads(part_file.read_text())
|
|
89
|
+
if part_data.get("type") == "text":
|
|
90
|
+
text = part_data.get("text", "")
|
|
91
|
+
if text.startswith('"') and text.endswith('"\n'):
|
|
92
|
+
text = json.loads(text.rstrip('\n'))
|
|
93
|
+
result.append({"role": role, "text": text})
|
|
94
|
+
break
|
|
95
|
+
except (json.JSONDecodeError, OSError):
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _format_handoff_message(
|
|
102
|
+
session_id: str,
|
|
103
|
+
messages: list[dict],
|
|
104
|
+
project: str | None = None,
|
|
105
|
+
) -> str:
|
|
106
|
+
lines = ["📱 **会话接力**", ""]
|
|
107
|
+
|
|
108
|
+
if project:
|
|
109
|
+
lines.append(f"📁 项目: `{project}`")
|
|
110
|
+
lines.append(f"🔗 Session: `{session_id}`")
|
|
111
|
+
lines.append("")
|
|
112
|
+
lines.append("---")
|
|
113
|
+
lines.append("")
|
|
114
|
+
|
|
115
|
+
for msg in messages:
|
|
116
|
+
role = msg.get("role", "unknown")
|
|
117
|
+
text = msg.get("text", "")
|
|
118
|
+
role_label = "👤" if role == "user" else "🤖"
|
|
119
|
+
if len(text) > 500:
|
|
120
|
+
text = text[:500] + "..."
|
|
121
|
+
lines.append(f"{role_label} **{role}**:")
|
|
122
|
+
lines.append(text)
|
|
123
|
+
lines.append("")
|
|
124
|
+
|
|
125
|
+
total_len = sum(len(line) for line in lines)
|
|
126
|
+
if total_len > 3500:
|
|
127
|
+
lines = lines[:20]
|
|
128
|
+
lines.append("... (truncated)")
|
|
129
|
+
|
|
130
|
+
lines.append("---")
|
|
131
|
+
lines.append("")
|
|
132
|
+
lines.append("💡 直接在此 Topic 发消息即可继续对话")
|
|
133
|
+
|
|
134
|
+
return "\n".join(lines)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def _create_handoff_topic(
|
|
138
|
+
token: str,
|
|
139
|
+
chat_id: int,
|
|
140
|
+
session_id: str,
|
|
141
|
+
project: str,
|
|
142
|
+
config_path: Path,
|
|
143
|
+
) -> int | None:
|
|
144
|
+
title = f"📱 {project} handoff"
|
|
145
|
+
|
|
146
|
+
client = TelegramClient(token)
|
|
147
|
+
try:
|
|
148
|
+
result = await client.create_forum_topic(chat_id, title)
|
|
149
|
+
if result is None:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
thread_id = result.message_thread_id
|
|
153
|
+
|
|
154
|
+
state_path = resolve_state_path(config_path)
|
|
155
|
+
store = TopicStateStore(state_path)
|
|
156
|
+
|
|
157
|
+
context = RunContext(project=project.lower(), branch=None)
|
|
158
|
+
await store.set_context(chat_id, thread_id, context, topic_title=title)
|
|
159
|
+
|
|
160
|
+
resume_token = ResumeToken(engine="opencode", value=session_id)
|
|
161
|
+
await store.set_session_resume(chat_id, thread_id, resume_token)
|
|
162
|
+
|
|
163
|
+
return thread_id
|
|
164
|
+
finally:
|
|
165
|
+
await client.close()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
async def _send_to_telegram(
|
|
169
|
+
token: str,
|
|
170
|
+
chat_id: int,
|
|
171
|
+
message: str,
|
|
172
|
+
thread_id: int | None = None,
|
|
173
|
+
) -> bool:
|
|
174
|
+
client = TelegramClient(token)
|
|
175
|
+
try:
|
|
176
|
+
result = await client.send_message(
|
|
177
|
+
chat_id=chat_id,
|
|
178
|
+
text=message,
|
|
179
|
+
message_thread_id=thread_id,
|
|
180
|
+
parse_mode="Markdown",
|
|
181
|
+
)
|
|
182
|
+
return result is not None
|
|
183
|
+
finally:
|
|
184
|
+
await client.close()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@app.command()
|
|
188
|
+
def send(
|
|
189
|
+
session: str | None = typer.Option(
|
|
190
|
+
None, "--session", "-s", help="Session ID (defaults to latest)"
|
|
191
|
+
),
|
|
192
|
+
limit: int = typer.Option(
|
|
193
|
+
3, "--limit", "-n", help="Number of messages to include"
|
|
194
|
+
),
|
|
195
|
+
project: str | None = typer.Option(
|
|
196
|
+
None, "--project", "-p", help="Project name for context"
|
|
197
|
+
),
|
|
198
|
+
) -> None:
|
|
199
|
+
result = load_settings_if_exists()
|
|
200
|
+
if result is None:
|
|
201
|
+
typer.echo("❌ 未找到 yee88 配置文件", err=True)
|
|
202
|
+
raise typer.Exit(1)
|
|
203
|
+
|
|
204
|
+
settings, config_path = result
|
|
205
|
+
telegram_cfg = settings.transports.telegram
|
|
206
|
+
|
|
207
|
+
token = telegram_cfg.bot_token
|
|
208
|
+
chat_id = telegram_cfg.chat_id
|
|
209
|
+
|
|
210
|
+
if not token or not chat_id:
|
|
211
|
+
typer.echo("❌ Telegram 配置不完整 (需要 bot_token 和 chat_id)", err=True)
|
|
212
|
+
raise typer.Exit(1)
|
|
213
|
+
|
|
214
|
+
if not telegram_cfg.topics.enabled:
|
|
215
|
+
typer.echo("❌ Topics 未启用,请先运行: yee88 config set transports.telegram.topics.enabled true", err=True)
|
|
216
|
+
raise typer.Exit(1)
|
|
217
|
+
|
|
218
|
+
session_id = session
|
|
219
|
+
session_project = project
|
|
220
|
+
if session_id is None:
|
|
221
|
+
sessions = _get_recent_sessions(limit=10)
|
|
222
|
+
if not sessions:
|
|
223
|
+
typer.echo("❌ 未找到 OpenCode 会话", err=True)
|
|
224
|
+
raise typer.Exit(1)
|
|
225
|
+
|
|
226
|
+
typer.echo("\n📲 会话接力 - 将电脑端会话发送到 Telegram 继续对话")
|
|
227
|
+
typer.echo("━" * 50)
|
|
228
|
+
typer.echo("\n📋 最近的会话:\n")
|
|
229
|
+
for i, s in enumerate(sessions[:10], 1):
|
|
230
|
+
title_display = s.title[:40] if s.title else s.project_name
|
|
231
|
+
typer.echo(f" [{i}] {s.updated_str} {title_display}")
|
|
232
|
+
typer.echo("")
|
|
233
|
+
|
|
234
|
+
choice = typer.prompt("选择会话 (1-10)", default="1")
|
|
235
|
+
try:
|
|
236
|
+
idx = int(choice) - 1
|
|
237
|
+
if idx < 0 or idx >= len(sessions):
|
|
238
|
+
typer.echo("❌ 无效选择", err=True)
|
|
239
|
+
raise typer.Exit(1)
|
|
240
|
+
except ValueError:
|
|
241
|
+
typer.echo("❌ 请输入数字", err=True)
|
|
242
|
+
raise typer.Exit(1)
|
|
243
|
+
|
|
244
|
+
selected = sessions[idx]
|
|
245
|
+
session_id = selected.id
|
|
246
|
+
if session_project is None:
|
|
247
|
+
session_project = selected.project_name
|
|
248
|
+
|
|
249
|
+
if not session_id:
|
|
250
|
+
typer.echo("❌ 会话 ID 为空", err=True)
|
|
251
|
+
raise typer.Exit(1)
|
|
252
|
+
|
|
253
|
+
typer.echo(f"📖 读取会话 {session_id[:20]}...")
|
|
254
|
+
|
|
255
|
+
messages = _get_session_messages(session_id, limit=limit)
|
|
256
|
+
if not messages:
|
|
257
|
+
typer.echo("❌ 无法读取会话消息", err=True)
|
|
258
|
+
raise typer.Exit(1)
|
|
259
|
+
|
|
260
|
+
typer.echo("🆕 创建新 Topic...")
|
|
261
|
+
|
|
262
|
+
async def do_handoff() -> tuple[bool, int | None]:
|
|
263
|
+
thread_id = await _create_handoff_topic(
|
|
264
|
+
token=token,
|
|
265
|
+
chat_id=chat_id,
|
|
266
|
+
session_id=session_id,
|
|
267
|
+
project=session_project or "unknown",
|
|
268
|
+
config_path=config_path,
|
|
269
|
+
)
|
|
270
|
+
if thread_id is None:
|
|
271
|
+
return False, None
|
|
272
|
+
|
|
273
|
+
handoff_msg = _format_handoff_message(
|
|
274
|
+
session_id=session_id,
|
|
275
|
+
messages=messages,
|
|
276
|
+
project=session_project,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
success = await _send_to_telegram(
|
|
280
|
+
token=token,
|
|
281
|
+
chat_id=chat_id,
|
|
282
|
+
message=handoff_msg,
|
|
283
|
+
thread_id=thread_id,
|
|
284
|
+
)
|
|
285
|
+
return success, thread_id
|
|
286
|
+
|
|
287
|
+
success, thread_id = anyio.run(do_handoff)
|
|
288
|
+
|
|
289
|
+
if success:
|
|
290
|
+
typer.echo("✅ 已发送到 Telegram!")
|
|
291
|
+
typer.echo(f" Session: {session_id}")
|
|
292
|
+
typer.echo(f" Project: {session_project}")
|
|
293
|
+
typer.echo(f" Topic ID: {thread_id}")
|
|
294
|
+
typer.echo(f" 消息数: {limit}")
|
|
295
|
+
else:
|
|
296
|
+
typer.echo("❌ 发送失败", err=True)
|
|
297
|
+
raise typer.Exit(1)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@app.callback(invoke_without_command=True)
|
|
301
|
+
def main(ctx: typer.Context) -> None:
|
|
302
|
+
"""Handoff session context to Telegram for mobile continuation."""
|
|
303
|
+
if ctx.invoked_subcommand is None:
|
|
304
|
+
ctx.invoke(send, session=None, limit=3, project=None)
|
yee88/cron/manager.py
CHANGED
|
@@ -2,25 +2,54 @@ import tomllib
|
|
|
2
2
|
import tomli_w
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import List, Optional
|
|
5
|
+
from zoneinfo import ZoneInfo
|
|
5
6
|
from croniter import croniter
|
|
6
7
|
from datetime import datetime
|
|
7
8
|
from .models import CronJob
|
|
8
9
|
|
|
10
|
+
BEIJING_TZ = ZoneInfo("Asia/Shanghai")
|
|
11
|
+
|
|
9
12
|
|
|
10
13
|
class CronManager:
|
|
11
|
-
def __init__(self, config_dir: Path):
|
|
14
|
+
def __init__(self, config_dir: Path, timezone: str = "Asia/Shanghai"):
|
|
12
15
|
self.file = config_dir / "cron.toml"
|
|
13
16
|
self.jobs: List[CronJob] = []
|
|
17
|
+
self.timezone = ZoneInfo(timezone)
|
|
14
18
|
|
|
15
19
|
def _validate_project(self, project: str) -> None:
|
|
16
20
|
if not project:
|
|
17
21
|
return
|
|
22
|
+
|
|
18
23
|
path = Path(project).expanduser().resolve()
|
|
19
24
|
if path.exists() and path.is_dir():
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
from ..settings import load_settings_if_exists
|
|
28
|
+
from ..engines import list_backend_ids
|
|
29
|
+
|
|
30
|
+
result = load_settings_if_exists()
|
|
31
|
+
if result is None:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
settings, config_path = result
|
|
35
|
+
engine_ids = list_backend_ids()
|
|
36
|
+
projects_config = settings.to_projects_config(
|
|
37
|
+
config_path=config_path,
|
|
38
|
+
engine_ids=engine_ids
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if project.lower() in projects_config.projects:
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
available = list(projects_config.projects.keys())
|
|
45
|
+
if available:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"未知项目: {project}。可用项目: {', '.join(available)}"
|
|
48
|
+
)
|
|
49
|
+
else:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"未知项目: {project}。请先使用 'yee88 init' 注册项目"
|
|
52
|
+
)
|
|
24
53
|
|
|
25
54
|
def load(self):
|
|
26
55
|
if not self.file.exists():
|
|
@@ -99,7 +128,7 @@ class CronManager:
|
|
|
99
128
|
return False
|
|
100
129
|
|
|
101
130
|
def get_due_jobs(self) -> List[CronJob]:
|
|
102
|
-
now = datetime.now()
|
|
131
|
+
now = datetime.now(self.timezone)
|
|
103
132
|
due = []
|
|
104
133
|
one_time_completed = []
|
|
105
134
|
|
|
@@ -107,30 +136,38 @@ class CronManager:
|
|
|
107
136
|
if not job.enabled:
|
|
108
137
|
continue
|
|
109
138
|
|
|
110
|
-
# 一次性任务处理
|
|
111
139
|
if job.one_time:
|
|
112
140
|
try:
|
|
113
141
|
exec_time = datetime.fromisoformat(job.schedule)
|
|
142
|
+
if exec_time.tzinfo is None:
|
|
143
|
+
exec_time = exec_time.replace(tzinfo=self.timezone)
|
|
114
144
|
if exec_time <= now:
|
|
115
145
|
due.append(job)
|
|
116
146
|
one_time_completed.append(job.id)
|
|
117
147
|
except Exception:
|
|
118
148
|
continue
|
|
119
149
|
else:
|
|
120
|
-
# 周期性任务处理
|
|
121
150
|
try:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
151
|
+
if job.last_run:
|
|
152
|
+
base = datetime.fromisoformat(job.last_run)
|
|
153
|
+
if base.tzinfo is None:
|
|
154
|
+
base = base.replace(tzinfo=self.timezone)
|
|
155
|
+
itr = croniter(job.schedule, base)
|
|
156
|
+
next_run = itr.get_next(datetime)
|
|
157
|
+
if next_run <= now:
|
|
158
|
+
due.append(job)
|
|
159
|
+
job.last_run = now.isoformat()
|
|
160
|
+
job.next_run = itr.get_next(datetime).isoformat()
|
|
161
|
+
else:
|
|
162
|
+
itr = croniter(job.schedule, now)
|
|
163
|
+
prev_run = itr.get_prev(datetime)
|
|
164
|
+
if prev_run.date() == now.date():
|
|
165
|
+
due.append(job)
|
|
166
|
+
job.last_run = now.isoformat()
|
|
167
|
+
job.next_run = itr.get_next(datetime).isoformat()
|
|
130
168
|
except Exception:
|
|
131
169
|
continue
|
|
132
170
|
|
|
133
|
-
# 删除已完成的一次性任务
|
|
134
171
|
if one_time_completed:
|
|
135
172
|
self.jobs = [j for j in self.jobs if j.id not in one_time_completed]
|
|
136
173
|
|
yee88/ids.py
CHANGED
|
@@ -7,7 +7,7 @@ _ID_RE = re.compile(ID_PATTERN)
|
|
|
7
7
|
|
|
8
8
|
RESERVED_CLI_COMMANDS = frozenset({"config", "doctor", "init", "plugins"})
|
|
9
9
|
RESERVED_CHAT_COMMANDS = frozenset(
|
|
10
|
-
{"cancel", "file", "new", "agent", "model", "reasoning", "trigger", "topic", "ctx"}
|
|
10
|
+
{"cancel", "file", "new", "fork", "agent", "model", "reasoning", "trigger", "topic", "ctx"}
|
|
11
11
|
)
|
|
12
12
|
RESERVED_ENGINE_IDS = RESERVED_CLI_COMMANDS | RESERVED_CHAT_COMMANDS
|
|
13
13
|
RESERVED_COMMAND_IDS = RESERVED_CLI_COMMANDS | RESERVED_CHAT_COMMANDS
|
yee88/skills/yee88/SKILL.md
CHANGED
|
@@ -36,13 +36,38 @@ description: 当用户说"提醒我"、"X分钟/小时后"、"定时"、"每天/
|
|
|
36
36
|
|
|
37
37
|
## ⛔ 默认不传 --project!除非用户明确要求
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
### 🧠 COT:判断是否需要 --project
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
**收到定时任务请求时,先在脑中过一遍这个决策流程:**
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
1. 用户有没有提到具体项目名?
|
|
45
|
+
- "在 takopi 项目下..." → 需要 --project
|
|
46
|
+
- "帮我在 work 项目..." → 需要 --project
|
|
47
|
+
- "提醒我喝水" → 不需要 --project
|
|
48
|
+
- "每天9点提醒站会" → 不需要 --project
|
|
49
|
+
|
|
50
|
+
2. 用户有没有说"在某个项目下运行"?
|
|
51
|
+
- "在 xxx 下执行..." → 需要 --project
|
|
52
|
+
- "切到 xxx 项目..." → 需要 --project
|
|
53
|
+
- 没有提到项目上下文 → 不需要 --project
|
|
54
|
+
|
|
55
|
+
3. 这个任务是通用提醒还是项目相关?
|
|
56
|
+
- 喝水、休息、开会 → 通用,不需要 --project
|
|
57
|
+
- 代码审查、部署、PR → 可能需要,但除非用户说了项目名,否则不传
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**结论:99% 的情况不需要 --project。只有用户明确说了项目名才传。**
|
|
61
|
+
|
|
62
|
+
### 决策表
|
|
63
|
+
|
|
64
|
+
| 用户说的话 | 需要 --project? | 命令 |
|
|
65
|
+
|-----------|----------------|------|
|
|
66
|
+
| "1分钟后提醒我喝水" | ❌ 不需要 | `yee88 cron add reminder "+1m" "喝水" -o` |
|
|
67
|
+
| "每天9点提醒我站会" | ❌ 不需要 | `yee88 cron add standup "0 9 * * *" "站会时间"` |
|
|
68
|
+
| "30分钟后提醒我休息" | ❌ 不需要 | `yee88 cron add break "+30m" "休息一下" -o` |
|
|
69
|
+
| "在 takopi 项目下每天9点跑测试" | ✅ 需要 | `yee88 cron add test "0 9 * * *" "跑测试" --project takopi` |
|
|
70
|
+
| "帮我在 work 项目设个提醒" | ✅ 需要 | `yee88 cron add reminder "+1h" "..." --project work -o` |
|
|
46
71
|
|
|
47
72
|
**⚠️ --project 只接受项目别名,不是路径!**
|
|
48
73
|
|
|
@@ -500,12 +525,38 @@ yee88 cron run <task-id>
|
|
|
500
525
|
- 执行后自动从列表中删除
|
|
501
526
|
- 无法对一次性任务使用 enable/disable(执行前自动删除)
|
|
502
527
|
|
|
528
|
+
### 10. 会话接力(Handoff)
|
|
529
|
+
|
|
530
|
+
将当前 OpenCode 会话上下文发送到 Telegram,方便在手机上继续对话。
|
|
531
|
+
|
|
532
|
+
```bash
|
|
533
|
+
yee88 handoff
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
功能:
|
|
537
|
+
- 自动列出当前项目的最近会话(带话题名称)
|
|
538
|
+
- 选择会话后,创建新的 Telegram Topic
|
|
539
|
+
- 将会话上下文和最近消息发送到 Topic
|
|
540
|
+
- 在 Telegram 中直接继续对话
|
|
541
|
+
|
|
542
|
+
选项:
|
|
543
|
+
- `--session, -s`: 指定会话 ID(默认交互选择)
|
|
544
|
+
- `--limit, -n`: 包含的消息数量(默认 3)
|
|
545
|
+
- `--project, -p`: 项目名称
|
|
546
|
+
|
|
547
|
+
示例:
|
|
548
|
+
```bash
|
|
549
|
+
yee88 handoff
|
|
550
|
+
yee88 handoff -s ses_abc123 -n 5
|
|
551
|
+
```
|
|
552
|
+
|
|
503
553
|
## 完整命令速查表
|
|
504
554
|
|
|
505
555
|
| 命令 | 说明 |
|
|
506
556
|
|------|------|
|
|
507
557
|
| `yee88` | 启动 yee88 |
|
|
508
558
|
| `yee88 init <alias>` | 注册项目 |
|
|
559
|
+
| `yee88 handoff` | 会话接力到 Telegram |
|
|
509
560
|
| `yee88 config path` | 查看配置路径 |
|
|
510
561
|
| `yee88 config list` | 列出配置 |
|
|
511
562
|
| `yee88 config get <key>` | 获取配置项 |
|
|
@@ -18,6 +18,7 @@ from .reasoning import _handle_reasoning_command as handle_reasoning_command
|
|
|
18
18
|
from .topics import _handle_chat_new_command as handle_chat_new_command
|
|
19
19
|
from .topics import _handle_chat_ctx_command as handle_chat_ctx_command
|
|
20
20
|
from .topics import _handle_ctx_command as handle_ctx_command
|
|
21
|
+
from .topics import _handle_fork_command as handle_fork_command
|
|
21
22
|
from .topics import _handle_new_command as handle_new_command
|
|
22
23
|
from .topics import _handle_topic_command as handle_topic_command
|
|
23
24
|
from .trigger import _handle_trigger_command as handle_trigger_command
|
|
@@ -31,6 +32,7 @@ __all__ = [
|
|
|
31
32
|
"handle_ctx_command",
|
|
32
33
|
"handle_file_command",
|
|
33
34
|
"handle_file_put_default",
|
|
35
|
+
"handle_fork_command",
|
|
34
36
|
"handle_media_group",
|
|
35
37
|
"handle_model_command",
|
|
36
38
|
"handle_new_command",
|
yee88/telegram/commands/menu.py
CHANGED
|
@@ -83,7 +83,10 @@ def build_bot_commands(
|
|
|
83
83
|
commands.append({"command": cmd, "description": description})
|
|
84
84
|
seen.add(cmd)
|
|
85
85
|
if include_topics:
|
|
86
|
-
for cmd, description in [
|
|
86
|
+
for cmd, description in [
|
|
87
|
+
("topic", "create or bind a topic"),
|
|
88
|
+
("fork", "fork current topic to new topic"),
|
|
89
|
+
]:
|
|
87
90
|
if cmd in seen:
|
|
88
91
|
continue
|
|
89
92
|
commands.append({"command": cmd, "description": description})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re as _re
|
|
3
4
|
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
from ...context import RunContext
|
|
@@ -340,3 +341,84 @@ async def _handle_topic_command(
|
|
|
340
341
|
message=RenderedMessage(text=rendered_text, extra={"entities": entities}),
|
|
341
342
|
options=SendOptions(thread_id=thread_id),
|
|
342
343
|
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
_FORK_TITLE_PATTERN = _re.compile(r"^(.+) \(fork #(\d+)\)$")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _get_forked_title(title: str | None) -> str:
|
|
350
|
+
if title is None:
|
|
351
|
+
return "topic (fork #1)"
|
|
352
|
+
match = _FORK_TITLE_PATTERN.match(title)
|
|
353
|
+
if match:
|
|
354
|
+
base = match.group(1)
|
|
355
|
+
num = int(match.group(2))
|
|
356
|
+
return f"{base} (fork #{num + 1})"
|
|
357
|
+
return f"{title} (fork #1)"
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
async def _handle_fork_command(
|
|
361
|
+
cfg: TelegramBridgeConfig,
|
|
362
|
+
msg: TelegramIncomingMessage,
|
|
363
|
+
store: TopicStateStore,
|
|
364
|
+
*,
|
|
365
|
+
resolved_scope: str | None = None,
|
|
366
|
+
scope_chat_ids: frozenset[int] | None = None,
|
|
367
|
+
) -> None:
|
|
368
|
+
from ...model import ResumeToken
|
|
369
|
+
|
|
370
|
+
reply = make_reply(cfg, msg)
|
|
371
|
+
error = _topics_command_error(
|
|
372
|
+
cfg,
|
|
373
|
+
msg.chat_id,
|
|
374
|
+
resolved_scope=resolved_scope,
|
|
375
|
+
scope_chat_ids=scope_chat_ids,
|
|
376
|
+
)
|
|
377
|
+
if error is not None:
|
|
378
|
+
await reply(text=error)
|
|
379
|
+
return
|
|
380
|
+
tkey = _topic_key(msg, cfg, scope_chat_ids=scope_chat_ids)
|
|
381
|
+
if tkey is None:
|
|
382
|
+
await reply(text="this command only works inside a topic.")
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
snapshot = await store.get_thread(*tkey)
|
|
386
|
+
current_title = snapshot.topic_title if snapshot else None
|
|
387
|
+
forked_title = _get_forked_title(current_title)
|
|
388
|
+
|
|
389
|
+
created = await cfg.bot.create_forum_topic(msg.chat_id, forked_title)
|
|
390
|
+
if created is None:
|
|
391
|
+
await reply(text="failed to create forked topic.")
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
new_thread_id = created.message_thread_id
|
|
395
|
+
|
|
396
|
+
if snapshot and snapshot.context:
|
|
397
|
+
await store.set_context(
|
|
398
|
+
msg.chat_id,
|
|
399
|
+
new_thread_id,
|
|
400
|
+
snapshot.context,
|
|
401
|
+
topic_title=forked_title,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
if snapshot and snapshot.sessions:
|
|
405
|
+
for engine, resume_value in snapshot.sessions.items():
|
|
406
|
+
token = ResumeToken(engine=engine, value=resume_value)
|
|
407
|
+
await store.set_session_resume(msg.chat_id, new_thread_id, token)
|
|
408
|
+
|
|
409
|
+
await reply(text=f"forked to new topic `{forked_title}`.")
|
|
410
|
+
|
|
411
|
+
context_label = _format_context(cfg.runtime, snapshot.context) if snapshot and snapshot.context else "none"
|
|
412
|
+
sessions_label = ", ".join(sorted(snapshot.sessions.keys())) if snapshot and snapshot.sessions else "none"
|
|
413
|
+
welcome_text = (
|
|
414
|
+
f"forked from previous topic\n\n"
|
|
415
|
+
f"context: `{context_label}`\n"
|
|
416
|
+
f"sessions: {sessions_label}\n\n"
|
|
417
|
+
"continue your conversation here!"
|
|
418
|
+
)
|
|
419
|
+
rendered_text, entities = prepare_telegram(MarkdownParts(header=welcome_text))
|
|
420
|
+
await cfg.exec_cfg.transport.send(
|
|
421
|
+
channel_id=msg.chat_id,
|
|
422
|
+
message=RenderedMessage(text=rendered_text, extra={"entities": entities}),
|
|
423
|
+
options=SendOptions(thread_id=new_thread_id),
|
|
424
|
+
)
|
yee88/telegram/loop.py
CHANGED
|
@@ -44,6 +44,7 @@ from .commands.handlers import (
|
|
|
44
44
|
handle_ctx_command,
|
|
45
45
|
handle_file_command,
|
|
46
46
|
handle_file_put_default,
|
|
47
|
+
handle_fork_command,
|
|
47
48
|
handle_media_group,
|
|
48
49
|
handle_model_command,
|
|
49
50
|
handle_new_command,
|
|
@@ -224,6 +225,15 @@ def _dispatch_builtin_command(
|
|
|
224
225
|
resolved_scope=resolved_scope,
|
|
225
226
|
scope_chat_ids=scope_chat_ids,
|
|
226
227
|
)
|
|
228
|
+
elif command_id == "fork":
|
|
229
|
+
handler = partial(
|
|
230
|
+
handle_fork_command,
|
|
231
|
+
cfg,
|
|
232
|
+
msg,
|
|
233
|
+
topic_store,
|
|
234
|
+
resolved_scope=resolved_scope,
|
|
235
|
+
scope_chat_ids=scope_chat_ids,
|
|
236
|
+
)
|
|
227
237
|
elif command_id == "topic":
|
|
228
238
|
handler = partial(
|
|
229
239
|
handle_topic_command,
|
|
@@ -1077,7 +1087,8 @@ async def run_main_loop(
|
|
|
1077
1087
|
tg.start_soon(run_config_watch)
|
|
1078
1088
|
|
|
1079
1089
|
if config_path is not None:
|
|
1080
|
-
cron_manager = CronManager(config_path.parent)
|
|
1090
|
+
cron_manager = CronManager(config_path.parent, timezone="Asia/Shanghai")
|
|
1091
|
+
logger.info("cron.manager.initialized", timezone="Asia/Shanghai")
|
|
1081
1092
|
|
|
1082
1093
|
async def _execute_cron_job(job: CronJob) -> None:
|
|
1083
1094
|
try:
|
|
@@ -10,7 +10,7 @@ yee88/context.py,sha256=2GXdWY8KpWbEARJNo34unLN8nqGBtiE72Z_mjwrJTUM,187
|
|
|
10
10
|
yee88/directives.py,sha256=28XG1f7yQs8GAgvPoy5g0pTe_aaVQeWFSD4-S5mC5oo,4583
|
|
11
11
|
yee88/engines.py,sha256=uXtGie-XkQDB1LROTjPlCkYKbCu7XgyTr0yGn837gN4,1646
|
|
12
12
|
yee88/events.py,sha256=1yo6l5D4xFreyPoU1d4L1ckaplPizxpFeJJevP8JDtk,4225
|
|
13
|
-
yee88/ids.py,sha256=
|
|
13
|
+
yee88/ids.py,sha256=n8ULXEUpeHRCGc571ZH2IAIGPRuf9lB0g1pZRFm8dWY,542
|
|
14
14
|
yee88/lockfile.py,sha256=xCSsxVQMqnCN2RWFkkDOVg1qm5D-Sb4AMKr6FdP0kbY,4162
|
|
15
15
|
yee88/logging.py,sha256=aqoIClE5_vlASKXsc9FjrmsmBy-7jLYOomHheSWSadA,8227
|
|
16
16
|
yee88/markdown.py,sha256=__2qyoBXTBZVVD7DgZvjDaj67xQCJHMfU0yQ8jcY1pQ,9144
|
|
@@ -28,10 +28,11 @@ yee88/transport.py,sha256=hzVJJO4mk1CaXgzsxSiMDqYA8xKMPD-SSQKhHDnwlzE,1330
|
|
|
28
28
|
yee88/transport_runtime.py,sha256=3NCcnnHAollwTOwpQzA9pYIF2jKNEZx3qKUIvK3d0Zk,10986
|
|
29
29
|
yee88/transports.py,sha256=8igVsWgvx4d743SE98LyNuKYaci0-X61IqNiO4U-_Ck,2013
|
|
30
30
|
yee88/worktrees.py,sha256=KDaT1S0-kQH5v0FmiWgG51jgGviyATVEgfoqRrLmh7Y,3907
|
|
31
|
-
yee88/cli/__init__.py,sha256=
|
|
31
|
+
yee88/cli/__init__.py,sha256=_Ji5MfYqvF2nuC7_sgbtZOOe0pvIuk0RQ47Hw_aCV_k,6684
|
|
32
32
|
yee88/cli/config.py,sha256=gTDdaTkJGGooeGECgcOXkpF-Y7Kmqi_Iyum0ghQzazg,9560
|
|
33
33
|
yee88/cli/cron.py,sha256=uvzgkAmMom8sDMjLgdMcB8V7DACmmdoGTiefif1Ol3U,6215
|
|
34
34
|
yee88/cli/doctor.py,sha256=FPLXk-ZSPZiHtWrncEqborcz4e1dhb3S5sUVcOw-n60,6000
|
|
35
|
+
yee88/cli/handoff.py,sha256=gd_4HE98wooT4n9gM2if5LkXZMoMi47Fd3A5U4ogBzE,9417
|
|
35
36
|
yee88/cli/init.py,sha256=y1Z08rh9t4RPhzsgJRtopgd6UvjEPYi9Wfv_t2KlrxI,3761
|
|
36
37
|
yee88/cli/onboarding_cmd.py,sha256=VNJl9cpfz-Bo7LNpGUIH-VtbOFdATTn1ry21cwtyQQM,4170
|
|
37
38
|
yee88/cli/plugins.py,sha256=Kxr1FgLIjN6c8il83_cfE9nix0jYPUvI6KXtol3Swow,6319
|
|
@@ -39,7 +40,7 @@ yee88/cli/reload.py,sha256=ays9R2kJ0IQEPGfxb3BY7R2mxbJBmp325dKrqqv1nw8,3552
|
|
|
39
40
|
yee88/cli/run.py,sha256=qKMJCEwoiNN4Qqt59Gmm1bDQHTboGf8JO73gMeACAck,14197
|
|
40
41
|
yee88/cli/topic.py,sha256=6j_o0wpHMfkyeyj9wcjU5T5PVXw_cOM0qM5fsMDRiIw,11019
|
|
41
42
|
yee88/cron/__init__.py,sha256=WZN2RcqJD4i6ZPZjI6b_sKI1VxhNGzhDu2Pju5u9J0o,152
|
|
42
|
-
yee88/cron/manager.py,sha256=
|
|
43
|
+
yee88/cron/manager.py,sha256=MOKYxGMEDH-rMMdd5nZmltp1kimzrO9L8OuV-HIoKnc,5500
|
|
43
44
|
yee88/cron/models.py,sha256=IZtUvMBr2_wZbgEWR3U9yQW-QEOMzUybDER8B7Ne8Go,224
|
|
44
45
|
yee88/cron/scheduler.py,sha256=ITNqpF4MXUieuuFBvVHgzOVxxR5bnhGAFYvWTJWy-IU,1922
|
|
45
46
|
yee88/runners/__init__.py,sha256=McKaMqLXT9dJlgiEwKf6biD0Ns66Fk7SrxwtcP0ZgzI,30
|
|
@@ -55,7 +56,7 @@ yee88/schemas/claude.py,sha256=HqOik1O4u3WcMb4RN2cTVJw6VRYn3EaYj8h5Sevs1XY,5702
|
|
|
55
56
|
yee88/schemas/codex.py,sha256=bgIsh35LuaqoOYdTl6BWR-mn-mvh3ouwes_Jn9JMVXg,3412
|
|
56
57
|
yee88/schemas/opencode.py,sha256=ODhnKXTzxZ_8qaQ7AYqXB7J-XoAjQnXbGMBVTUEM2qY,1175
|
|
57
58
|
yee88/schemas/pi.py,sha256=e5ETawxk8jrdJbEbeBI_pWQKeCFiBBZLEF-Wo5Fx0XY,2680
|
|
58
|
-
yee88/skills/yee88/SKILL.md,sha256=
|
|
59
|
+
yee88/skills/yee88/SKILL.md,sha256=GrZ7a8sm2ArUo5OdeUC3OECavnykx2DcsBI5EdtdvuQ,15113
|
|
59
60
|
yee88/telegram/__init__.py,sha256=hX_Wvu920dNLTDrKlj0fsZFSewOo-_otN26O1UNPNA4,452
|
|
60
61
|
yee88/telegram/api_models.py,sha256=d3H4o2sRZuFgfZfxF1Y1z_xzzCoOy3bKhMeKggYCW3w,538
|
|
61
62
|
yee88/telegram/api_schemas.py,sha256=Xj-i68GFSb9Uk6GhU2Etp-gYhAat9DKYcvQN9av1NPQ,3823
|
|
@@ -69,7 +70,7 @@ yee88/telegram/context.py,sha256=Hb8-k-YbAjO0EmZ35hA8yyMvl1kozgYHUL1L-lbCglA,468
|
|
|
69
70
|
yee88/telegram/engine_defaults.py,sha256=n6ROkTmP_s-H5AhPz_OdT62oZf0QtZJyFEDjp5gfub4,2594
|
|
70
71
|
yee88/telegram/engine_overrides.py,sha256=kv2j102VP-Bqzbutd5ApBkjW3LmVwvCYixsFewVXVeY,3122
|
|
71
72
|
yee88/telegram/files.py,sha256=Cvmw6r_ocSb3jLzJLGVbzr46m8cRU159majJ1-A5lvg,5053
|
|
72
|
-
yee88/telegram/loop.py,sha256=
|
|
73
|
+
yee88/telegram/loop.py,sha256=pjSIsvIAg4N-83EjNHkDnoz8LHF0rQkwkdCN8NAcNZY,69778
|
|
73
74
|
yee88/telegram/onboarding.py,sha256=QWYaJT_s2bujDxzKjsZuLytyxs_XFDRuiBrsZGRjoOw,35633
|
|
74
75
|
yee88/telegram/outbox.py,sha256=OcoRyQ7zmQCXR8ZXEMK2f_7-UMRVRAbBgmJGS1u_lcU,5939
|
|
75
76
|
yee88/telegram/parsing.py,sha256=5PvIPns1NnKryt3XNxPCp4BpWX1gI8kjKi4VxcQ0W-Q,7429
|
|
@@ -86,16 +87,16 @@ yee88/telegram/commands/cancel.py,sha256=jE93VjztNETlmAgb7FJX0sLR3O-NHy5XcraUbK1
|
|
|
86
87
|
yee88/telegram/commands/dispatch.py,sha256=zcvX0V6_klf5jXqJlBwAK25e83WC9z3-KoRcDbeWre0,3572
|
|
87
88
|
yee88/telegram/commands/executor.py,sha256=eczs7Ds_S8GVX2_OvdmN23y6zE1fhYTpIAuchfH0eRU,14781
|
|
88
89
|
yee88/telegram/commands/file_transfer.py,sha256=yfopf_If9f7Scaz4dlUsfcrVvg24QmQdajLzemaENy0,17697
|
|
89
|
-
yee88/telegram/commands/handlers.py,sha256=
|
|
90
|
+
yee88/telegram/commands/handlers.py,sha256=KsmV6U_TKeNBvy5UdjuC4DQ3_xYiw_CFIYNrrQYlUfQ,1925
|
|
90
91
|
yee88/telegram/commands/media.py,sha256=drSKTf_BbyvXOGhS9UKwey_243Ic5XissoaxCykpd-c,5045
|
|
91
|
-
yee88/telegram/commands/menu.py,sha256=
|
|
92
|
+
yee88/telegram/commands/menu.py,sha256=vkvH0E4-sizhtyLgq5YRcptcWkzsVgMHkADxkQGscFo,4546
|
|
92
93
|
yee88/telegram/commands/model.py,sha256=GwdEHuebm02aUIqkcvznPh0FSViVlAjxeMks7TjFXIE,12729
|
|
93
94
|
yee88/telegram/commands/overrides.py,sha256=lLlIuCWkKwbS5WlQOOr_Ftv_EvfHR9DhjBpWaf6FBng,4853
|
|
94
95
|
yee88/telegram/commands/parse.py,sha256=0QVW1TVdBWadLbpJ9lRY3s7W4Cm62JJa9jfAaFHQmXU,887
|
|
95
96
|
yee88/telegram/commands/plan.py,sha256=iKsaRBS-qIfvAaxik5ZEA_VzAnFwx7aEED8sKXNq1wE,487
|
|
96
97
|
yee88/telegram/commands/reasoning.py,sha256=UFEJOHm4d0v2jFh5HC3oQGS41NYKbmJHRTaAmu_LiGo,8188
|
|
97
98
|
yee88/telegram/commands/reply.py,sha256=a3zkNjKzn3qZXEZFXuflX-tdhQKQyhYDqZskMy1nS3o,580
|
|
98
|
-
yee88/telegram/commands/topics.py,sha256=
|
|
99
|
+
yee88/telegram/commands/topics.py,sha256=TBUdhqtTyDv7I6qFlxeENPMfvZGkYUjvHsEI2Q2b2Mc,13899
|
|
99
100
|
yee88/telegram/commands/trigger.py,sha256=RgB4V7NoFdfDLk83xl3BPTsIIVr5A2zCNstR_5B_mEw,5201
|
|
100
101
|
yee88/utils/__init__.py,sha256=cV9_7v07Y6XkrUju0lHdO1ia0-Q57NqyFVMaFCg--do,34
|
|
101
102
|
yee88/utils/git.py,sha256=SVKcPNl2mUrv-cVHZQ5b8QXWKi33e_Jc4_vfCLwagkg,2413
|
|
@@ -103,8 +104,8 @@ yee88/utils/json_state.py,sha256=cnSvGbB9zj90GdYSyigId1M0nEx54T3A3CqqhkAm9kQ,524
|
|
|
103
104
|
yee88/utils/paths.py,sha256=_Tp-LyFLeyGD0P0agRudLuT1NR_XTIpryxk3OYDJAGQ,1318
|
|
104
105
|
yee88/utils/streams.py,sha256=TQezA-A5VCNksLOtwsJplfr8vm1xPTXoGxvik8G2NPI,1121
|
|
105
106
|
yee88/utils/subprocess.py,sha256=2if6IxTZVSB1kDa8SXw3igj3E-zhKB8P4z5MVe-odzY,2169
|
|
106
|
-
yee88-0.
|
|
107
|
-
yee88-0.
|
|
108
|
-
yee88-0.
|
|
109
|
-
yee88-0.
|
|
110
|
-
yee88-0.
|
|
107
|
+
yee88-0.7.1.dist-info/METADATA,sha256=CcgQN8xjM0IfbL-tv1FY8HcH4jz-tcPV27VROzOGgOg,4340
|
|
108
|
+
yee88-0.7.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
109
|
+
yee88-0.7.1.dist-info/entry_points.txt,sha256=P4MVZ_sZfrHaARVMImNJjoGamP8VDukARWMKfDh20V8,282
|
|
110
|
+
yee88-0.7.1.dist-info/licenses/LICENSE,sha256=poyQ59wnbmL3Ox3TiiephfHvUpLvJl0DwLFFgqBDdHY,1063
|
|
111
|
+
yee88-0.7.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|