@hupan56/wlkj 3.1.25 → 3.1.26

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.
@@ -1,245 +1,245 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- QODER Pipeline - 会话级活跃任务管理
5
-
6
- 核心概念: "活跃任务" 是按 AI 会话/窗口存储的, 不是全局的。
7
-
8
- 存储位置: .qoder/.runtime/sessions/<session-id>
9
- 每个会话文件只包含一行: 活跃任务的相对路径。
10
-
11
- 会话身份来源 (按优先级):
12
- 1. 环境变量: QODER_SESSION_ID, CODEX_SESSION_ID, CODEX_THREAD_ID 等
13
- 2. Hook 输入中的 session_id 字段
14
- 3. TRELLIS_CONTEXT_ID 环境变量
15
- 4. 后备: 全局 .qoder/.current-task 文件
16
-
17
- 参考: Trellis 的 common/active_task.py 设计
18
- """
19
-
20
- from __future__ import annotations
21
-
22
- import hashlib
23
- import json
24
- import os
25
- import sys
26
- from dataclasses import dataclass
27
- from pathlib import Path
28
- from typing import Any
29
-
30
- # 支持的 AI 平台及其会话环境变量
31
- # -----------------------------------------------------------------------------
32
- # 单一事实源: nexus.registry.HOST_TABLE(宿主能力抽象层)。
33
- # 本表从 HOST_TABLE 派生,保持 tuple[tuple[str, tuple[str, ...]]] 格式兼容。
34
- # 新增宿主只需改 nexus.HOST_TABLE 一处,active_task 自动跟随。
35
- # nexus 不可用时回退到内联定义(向后兼容,零破坏)。
36
- # -----------------------------------------------------------------------------
37
- try:
38
- from capability.registry import HOST_TABLE as _HOST_TABLE
39
-
40
- _ENV_SESSION_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = tuple(
41
- (name, tuple(info.get("detect", ())))
42
- for name, info in _HOST_TABLE.items()
43
- if name != "cli" and info.get("detect")
44
- )
45
- except ImportError:
46
- _ENV_SESSION_KEYS = (
47
- ("qoderwork", ("QODER_SESSION_ID",)),
48
- ("codex", ("CODEX_SESSION_ID", "CODEX_THREAD_ID")),
49
- ("claude", ("CLAUDE_SESSION_ID", "CLAUDE_CODE_SESSION_ID")),
50
- ("cursor", ("CURSOR_SESSION_ID",)),
51
- ("opencode", ("OPENCODE_SESSION_ID", "OPENCODE_RUN_ID")),
52
- ("gemini", ("GEMINI_SESSION_ID",)),
53
- ("copilot", ("COPILOT_SESSION_ID",)),
54
- )
55
-
56
-
57
- @dataclass(frozen=True)
58
- class ActiveTask:
59
- """活跃任务状态。
60
-
61
- Attributes:
62
- task_path: 任务相对路径, None 表示无活跃任务。
63
- source_type: 来源类型 (session / global-fallback / none)。
64
- context_key: 会话标识键。
65
- stale: 是否过期。
66
- """
67
- task_path: str | None
68
- source_type: str # "session" | "global-fallback" | "none"
69
- context_key: str | None = None
70
- stale: bool = False
71
-
72
- @property
73
- def source(self) -> str:
74
- """人类可读的来源标签。"""
75
- if self.source_type == "session" and self.context_key:
76
- return f"session:{self.context_key}"
77
- if self.source_type == "global-fallback":
78
- return "global-fallback"
79
- return "none"
80
-
81
-
82
- # =============================================================================
83
- # 会话身份解析
84
- # =============================================================================
85
-
86
- def resolve_context_key() -> str | None:
87
- """解析当前会话的唯一标识键。
88
-
89
- 按平台优先级检查环境变量, 返回第一个找到的值。
90
-
91
- Returns:
92
- 会话 ID 字符串, 无则返回 None。
93
- """
94
- # 1. 检查通用环境变量
95
- for env_var in ("TRELLIS_CONTEXT_ID", "QODER_CONTEXT_ID"):
96
- val = os.environ.get(env_var)
97
- if val:
98
- return val
99
-
100
- # 2. 检查各平台专用环境变量
101
- for _platform, env_vars in _ENV_SESSION_KEYS:
102
- for env_var in env_vars:
103
- val = os.environ.get(env_var)
104
- if val:
105
- return val
106
-
107
- return None
108
-
109
-
110
- def _get_session_file(session_key: str, repo_root: Path) -> Path:
111
- """获取会话状态文件路径。
112
-
113
- Args:
114
- session_key: 会话标识。
115
- repo_root: 项目根目录。
116
-
117
- Returns:
118
- 会话文件路径。
119
- """
120
- # 对 session_key 做 hash 避免特殊字符问题
121
- key_hash = hashlib.sha256(session_key.encode()).hexdigest()[:16]
122
- sessions_dir = repo_root / ".qoder" / ".runtime" / "sessions"
123
- sessions_dir.mkdir(parents=True, exist_ok=True)
124
- return sessions_dir / f"session-{key_hash}"
125
-
126
-
127
- # =============================================================================
128
- # 活跃任务读写
129
- # =============================================================================
130
-
131
- def resolve_active_task(repo_root: Path | None = None) -> ActiveTask:
132
- """解析当前会话的活跃任务。
133
-
134
- 优先级:
135
- 1. 会话级状态文件
136
- 2. 全局 .current-task 文件 (后备)
137
-
138
- Args:
139
- repo_root: 项目根目录, 默认自动检测。
140
-
141
- Returns:
142
- ActiveTask 对象。
143
- """
144
- if repo_root is None:
145
- from foundation.core.paths import get_repo_root
146
- repo_root = get_repo_root()
147
-
148
- # 1. 尝试会话级
149
- session_key = resolve_context_key()
150
- if session_key:
151
- session_file = _get_session_file(session_key, repo_root)
152
- if session_file.is_file():
153
- try:
154
- task_path = session_file.read_text(encoding="utf-8").strip()
155
- if task_path:
156
- return ActiveTask(
157
- task_path=task_path,
158
- source_type="session",
159
- context_key=session_key,
160
- )
161
- except (OSError, IOError):
162
- pass
163
-
164
- # 2. 后备: 全局文件
165
- from foundation.core.paths import get_current_task
166
- global_task = get_current_task(repo_root)
167
- if global_task:
168
- return ActiveTask(
169
- task_path=global_task,
170
- source_type="global-fallback",
171
- context_key=session_key,
172
- )
173
-
174
- return ActiveTask(task_path=None, source_type="none")
175
-
176
-
177
- def set_active_task(task_path: str, repo_root: Path | None = None) -> ActiveTask | None:
178
- """设置当前会话的活跃任务。
179
-
180
- Args:
181
- task_path: 任务相对路径。
182
- repo_root: 项目根目录, 默认自动检测。
183
-
184
- Returns:
185
- ActiveTask 对象, 无会话身份则返回 None。
186
- """
187
- if repo_root is None:
188
- from foundation.core.paths import get_repo_root
189
- repo_root = get_repo_root()
190
-
191
- session_key = resolve_context_key()
192
- if not session_key:
193
- # 无会话身份, 使用全局后备
194
- from foundation.core.paths import set_current_task
195
- set_current_task(task_path, repo_root)
196
- return ActiveTask(
197
- task_path=task_path,
198
- source_type="global-fallback",
199
- context_key=None,
200
- )
201
-
202
- session_file = _get_session_file(session_key, repo_root)
203
- try:
204
- session_file.parent.mkdir(parents=True, exist_ok=True)
205
- session_file.write_text(task_path, encoding="utf-8")
206
- return ActiveTask(
207
- task_path=task_path,
208
- source_type="session",
209
- context_key=session_key,
210
- )
211
- except (OSError, IOError) as e:
212
- print(f"Warning: Failed to write session file: {e}", file=sys.stderr)
213
- return None
214
-
215
-
216
- def clear_active_task(repo_root: Path | None = None) -> ActiveTask:
217
- """清除当前会话的活跃任务。
218
-
219
- Args:
220
- repo_root: 项目根目录, 默认自动检测。
221
-
222
- Returns:
223
- 清除前的 ActiveTask 状态。
224
- """
225
- if repo_root is None:
226
- from foundation.core.paths import get_repo_root
227
- repo_root = get_repo_root()
228
-
229
- # 获取清除前的状态
230
- active = resolve_active_task(repo_root)
231
-
232
- # 清除会话文件
233
- session_key = resolve_context_key()
234
- if session_key:
235
- session_file = _get_session_file(session_key, repo_root)
236
- try:
237
- session_file.unlink(missing_ok=True)
238
- except (OSError, IOError):
239
- pass
240
-
241
- # 也清除全局后备
242
- from foundation.core.paths import set_current_task
243
- set_current_task(None, repo_root)
244
-
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ QODER Pipeline - 会话级活跃任务管理
5
+
6
+ 核心概念: "活跃任务" 是按 AI 会话/窗口存储的, 不是全局的。
7
+
8
+ 存储位置: .qoder/.runtime/sessions/<session-id>
9
+ 每个会话文件只包含一行: 活跃任务的相对路径。
10
+
11
+ 会话身份来源 (按优先级):
12
+ 1. 环境变量: QODER_SESSION_ID, CODEX_SESSION_ID, CODEX_THREAD_ID 等
13
+ 2. Hook 输入中的 session_id 字段
14
+ 3. TRELLIS_CONTEXT_ID 环境变量
15
+ 4. 后备: 全局 .qoder/.current-task 文件
16
+
17
+ 参考: common/active_task.py 设计
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import hashlib
23
+ import json
24
+ import os
25
+ import sys
26
+ from dataclasses import dataclass
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ # 支持的 AI 平台及其会话环境变量
31
+ # -----------------------------------------------------------------------------
32
+ # 单一事实源: nexus.registry.HOST_TABLE(宿主能力抽象层)。
33
+ # 本表从 HOST_TABLE 派生,保持 tuple[tuple[str, tuple[str, ...]]] 格式兼容。
34
+ # 新增宿主只需改 nexus.HOST_TABLE 一处,active_task 自动跟随。
35
+ # nexus 不可用时回退到内联定义(向后兼容,零破坏)。
36
+ # -----------------------------------------------------------------------------
37
+ try:
38
+ from capability.registry import HOST_TABLE as _HOST_TABLE
39
+
40
+ _ENV_SESSION_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = tuple(
41
+ (name, tuple(info.get("detect", ())))
42
+ for name, info in _HOST_TABLE.items()
43
+ if name != "cli" and info.get("detect")
44
+ )
45
+ except ImportError:
46
+ _ENV_SESSION_KEYS = (
47
+ ("qoderwork", ("QODER_SESSION_ID",)),
48
+ ("codex", ("CODEX_SESSION_ID", "CODEX_THREAD_ID")),
49
+ ("claude", ("CLAUDE_SESSION_ID", "CLAUDE_CODE_SESSION_ID")),
50
+ ("cursor", ("CURSOR_SESSION_ID",)),
51
+ ("opencode", ("OPENCODE_SESSION_ID", "OPENCODE_RUN_ID")),
52
+ ("gemini", ("GEMINI_SESSION_ID",)),
53
+ ("copilot", ("COPILOT_SESSION_ID",)),
54
+ )
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class ActiveTask:
59
+ """活跃任务状态。
60
+
61
+ Attributes:
62
+ task_path: 任务相对路径, None 表示无活跃任务。
63
+ source_type: 来源类型 (session / global-fallback / none)。
64
+ context_key: 会话标识键。
65
+ stale: 是否过期。
66
+ """
67
+ task_path: str | None
68
+ source_type: str # "session" | "global-fallback" | "none"
69
+ context_key: str | None = None
70
+ stale: bool = False
71
+
72
+ @property
73
+ def source(self) -> str:
74
+ """人类可读的来源标签。"""
75
+ if self.source_type == "session" and self.context_key:
76
+ return f"session:{self.context_key}"
77
+ if self.source_type == "global-fallback":
78
+ return "global-fallback"
79
+ return "none"
80
+
81
+
82
+ # =============================================================================
83
+ # 会话身份解析
84
+ # =============================================================================
85
+
86
+ def resolve_context_key() -> str | None:
87
+ """解析当前会话的唯一标识键。
88
+
89
+ 按平台优先级检查环境变量, 返回第一个找到的值。
90
+
91
+ Returns:
92
+ 会话 ID 字符串, 无则返回 None。
93
+ """
94
+ # 1. 检查通用环境变量
95
+ for env_var in ("TRELLIS_CONTEXT_ID", "QODER_CONTEXT_ID"):
96
+ val = os.environ.get(env_var)
97
+ if val:
98
+ return val
99
+
100
+ # 2. 检查各平台专用环境变量
101
+ for _platform, env_vars in _ENV_SESSION_KEYS:
102
+ for env_var in env_vars:
103
+ val = os.environ.get(env_var)
104
+ if val:
105
+ return val
106
+
107
+ return None
108
+
109
+
110
+ def _get_session_file(session_key: str, repo_root: Path) -> Path:
111
+ """获取会话状态文件路径。
112
+
113
+ Args:
114
+ session_key: 会话标识。
115
+ repo_root: 项目根目录。
116
+
117
+ Returns:
118
+ 会话文件路径。
119
+ """
120
+ # 对 session_key 做 hash 避免特殊字符问题
121
+ key_hash = hashlib.sha256(session_key.encode()).hexdigest()[:16]
122
+ sessions_dir = repo_root / ".qoder" / ".runtime" / "sessions"
123
+ sessions_dir.mkdir(parents=True, exist_ok=True)
124
+ return sessions_dir / f"session-{key_hash}"
125
+
126
+
127
+ # =============================================================================
128
+ # 活跃任务读写
129
+ # =============================================================================
130
+
131
+ def resolve_active_task(repo_root: Path | None = None) -> ActiveTask:
132
+ """解析当前会话的活跃任务。
133
+
134
+ 优先级:
135
+ 1. 会话级状态文件
136
+ 2. 全局 .current-task 文件 (后备)
137
+
138
+ Args:
139
+ repo_root: 项目根目录, 默认自动检测。
140
+
141
+ Returns:
142
+ ActiveTask 对象。
143
+ """
144
+ if repo_root is None:
145
+ from foundation.core.paths import get_repo_root
146
+ repo_root = get_repo_root()
147
+
148
+ # 1. 尝试会话级
149
+ session_key = resolve_context_key()
150
+ if session_key:
151
+ session_file = _get_session_file(session_key, repo_root)
152
+ if session_file.is_file():
153
+ try:
154
+ task_path = session_file.read_text(encoding="utf-8").strip()
155
+ if task_path:
156
+ return ActiveTask(
157
+ task_path=task_path,
158
+ source_type="session",
159
+ context_key=session_key,
160
+ )
161
+ except (OSError, IOError):
162
+ pass
163
+
164
+ # 2. 后备: 全局文件
165
+ from foundation.core.paths import get_current_task
166
+ global_task = get_current_task(repo_root)
167
+ if global_task:
168
+ return ActiveTask(
169
+ task_path=global_task,
170
+ source_type="global-fallback",
171
+ context_key=session_key,
172
+ )
173
+
174
+ return ActiveTask(task_path=None, source_type="none")
175
+
176
+
177
+ def set_active_task(task_path: str, repo_root: Path | None = None) -> ActiveTask | None:
178
+ """设置当前会话的活跃任务。
179
+
180
+ Args:
181
+ task_path: 任务相对路径。
182
+ repo_root: 项目根目录, 默认自动检测。
183
+
184
+ Returns:
185
+ ActiveTask 对象, 无会话身份则返回 None。
186
+ """
187
+ if repo_root is None:
188
+ from foundation.core.paths import get_repo_root
189
+ repo_root = get_repo_root()
190
+
191
+ session_key = resolve_context_key()
192
+ if not session_key:
193
+ # 无会话身份, 使用全局后备
194
+ from foundation.core.paths import set_current_task
195
+ set_current_task(task_path, repo_root)
196
+ return ActiveTask(
197
+ task_path=task_path,
198
+ source_type="global-fallback",
199
+ context_key=None,
200
+ )
201
+
202
+ session_file = _get_session_file(session_key, repo_root)
203
+ try:
204
+ session_file.parent.mkdir(parents=True, exist_ok=True)
205
+ session_file.write_text(task_path, encoding="utf-8")
206
+ return ActiveTask(
207
+ task_path=task_path,
208
+ source_type="session",
209
+ context_key=session_key,
210
+ )
211
+ except (OSError, IOError) as e:
212
+ print(f"Warning: Failed to write session file: {e}", file=sys.stderr)
213
+ return None
214
+
215
+
216
+ def clear_active_task(repo_root: Path | None = None) -> ActiveTask:
217
+ """清除当前会话的活跃任务。
218
+
219
+ Args:
220
+ repo_root: 项目根目录, 默认自动检测。
221
+
222
+ Returns:
223
+ 清除前的 ActiveTask 状态。
224
+ """
225
+ if repo_root is None:
226
+ from foundation.core.paths import get_repo_root
227
+ repo_root = get_repo_root()
228
+
229
+ # 获取清除前的状态
230
+ active = resolve_active_task(repo_root)
231
+
232
+ # 清除会话文件
233
+ session_key = resolve_context_key()
234
+ if session_key:
235
+ session_file = _get_session_file(session_key, repo_root)
236
+ try:
237
+ session_file.unlink(missing_ok=True)
238
+ except (OSError, IOError):
239
+ pass
240
+
241
+ # 也清除全局后备
242
+ from foundation.core.paths import set_current_task
243
+ set_current_task(None, repo_root)
244
+
245
245
  return active
@@ -1,6 +1,6 @@
1
1
  # QODER Pipeline
2
2
 
3
- ## 🏗️ 架构: 引擎与数据分离 (Trellis 化)
3
+ ## 🏗️ 架构: 引擎与数据分离 (引擎数据分离)
4
4
 
5
5
  `.qoder/` 一分为二 (v2.8.0+):
6
6