@hupan56/wlkj 2.2.4 → 2.2.6

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,39 +1,39 @@
1
- # role.py - Resolve role from member info
2
- import os, json, sys
3
-
4
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
5
- from common.paths import get_developer, get_developer_info
6
-
7
- BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8
-
9
- def get_role(name=None):
10
- if not name:
11
- # Prefer role recorded directly in .developer
12
- info = get_developer_info()
13
- if info and info.get('role'):
14
- return info['role']
15
- name = get_developer()
16
- if not name:
17
- return None
18
- mf = os.path.join(BASE, 'workspace', 'members', name, 'member.json')
19
- if os.path.isfile(mf):
20
- try:
21
- with open(mf, encoding='utf-8') as f:
22
- return json.load(f).get('role')
23
- except (OSError, json.JSONDecodeError):
24
- return None
25
- return None
26
-
27
- def get_permissions(role):
28
- import yaml
29
- cfg = os.path.join(BASE, '.qoder', 'config.yaml')
30
- if not os.path.isfile(cfg):
31
- return []
32
- try:
33
- with open(cfg, encoding='utf-8') as f:
34
- config = yaml.safe_load(f) or {}
35
- except (OSError, yaml.YAMLError):
36
- return []
37
- roles = config.get('roles', {})
38
- r = roles.get(role, {})
39
- return r.get('permissions', [])
1
+ # role.py - Resolve role from member info
2
+ import os, json, sys
3
+
4
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
5
+ from common.paths import get_developer, get_developer_info
6
+
7
+ BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8
+
9
+ def get_role(name=None):
10
+ if not name:
11
+ # Prefer role recorded directly in .developer
12
+ info = get_developer_info()
13
+ if info and info.get('role'):
14
+ return info['role']
15
+ name = get_developer()
16
+ if not name:
17
+ return None
18
+ mf = os.path.join(BASE, 'workspace', 'members', name, 'member.json')
19
+ if os.path.isfile(mf):
20
+ try:
21
+ with open(mf, encoding='utf-8') as f:
22
+ return json.load(f).get('role')
23
+ except (OSError, json.JSONDecodeError):
24
+ return None
25
+ return None
26
+
27
+ def get_permissions(role):
28
+ import yaml
29
+ cfg = os.path.join(BASE, '.qoder', 'config.yaml')
30
+ if not os.path.isfile(cfg):
31
+ return []
32
+ try:
33
+ with open(cfg, encoding='utf-8') as f:
34
+ config = yaml.safe_load(f) or {}
35
+ except (OSError, yaml.YAMLError):
36
+ return []
37
+ roles = config.get('roles', {})
38
+ r = roles.get(role, {})
39
+ return r.get('permissions', [])
@@ -78,7 +78,82 @@ def _search_cache_key(query, platform):
78
78
  return hashlib.md5(raw.encode('utf-8')).hexdigest()[:16]
79
79
 
80
80
 
81
+ def _check_index_freshness():
82
+ """轻量检查索引是否过期 (代码改了但索引没更新)。
83
+
84
+ 抽样 data/code/ 下最新修改的文件, 与 .index-meta.json 的 last_sync 比。
85
+ 过期则打印提示 (不阻塞搜索)。
86
+ 用一个静默文件标记避免每次搜索都提示 (.runtime/.index-stale-warned)。
87
+ """
88
+ try:
89
+ import datetime as _dt
90
+ meta_path = os.path.join(BASE, 'data', 'index', '.index-meta.json')
91
+ if not os.path.isfile(meta_path):
92
+ return
93
+ with open(meta_path, encoding='utf-8') as f:
94
+ meta = json.load(f)
95
+ last_sync_str = meta.get('last_sync', '')
96
+ if not last_sync_str:
97
+ return
98
+ last_sync = _dt.datetime.strptime(last_sync_str, '%Y-%m-%d %H:%M')
99
+
100
+ # 抽样: 找 data/code/ 下最新修改的 .java/.vue/.js 文件 (只扫一层子目录的最近文件)
101
+ code_dir = os.path.join(BASE, 'data', 'code')
102
+ if not os.path.isdir(code_dir):
103
+ return
104
+ newest_mtime = 0
105
+ for proj in os.listdir(code_dir):
106
+ proj_dir = os.path.join(code_dir, proj)
107
+ if not os.path.isdir(proj_dir):
108
+ continue
109
+ # 只看 src 目录下的文件 (避免 .git 干扰)
110
+ src_dir = os.path.join(proj_dir, 'src')
111
+ if not os.path.isdir(src_dir):
112
+ continue
113
+ # 抽样: os.walk 只走一层 (限制深度), 找最新 mtime
114
+ for root, dirs, files in os.walk(src_dir):
115
+ # 限制深度: 只走 3 层 (够采样, 不全扫)
116
+ depth = root[len(src_dir):].count(os.sep)
117
+ if depth >= 3:
118
+ dirs[:] = []
119
+ continue
120
+ for fn in files:
121
+ if fn.endswith(('.java', '.vue', '.js', '.ts')):
122
+ try:
123
+ mt = os.path.getmtime(os.path.join(root, fn))
124
+ if mt > newest_mtime:
125
+ newest_mtime = mt
126
+ except OSError:
127
+ pass
128
+ if newest_mtime > 0:
129
+ break # 拿到一个项目的最新就够了
130
+
131
+ if newest_mtime <= 0:
132
+ return
133
+ newest_dt = _dt.datetime.fromtimestamp(newest_mtime)
134
+ # 过期判定: 代码文件比索引新超过 1 小时
135
+ if (newest_dt - last_sync).total_seconds() > 3600:
136
+ # 静默标记: 避免每次搜索都提示 (索引更新后标记自动失效)
137
+ warned_path = os.path.join(BASE, '.qoder', '.runtime', '.index-stale-warned')
138
+ warn_mtime = os.path.getmtime(warned_path) if os.path.isfile(warned_path) else 0
139
+ meta_mtime = os.path.getmtime(meta_path)
140
+ if warn_mtime > meta_mtime:
141
+ return # 已经提示过了 (在本次索引周期内)
142
+ print('[提示] 代码有更新但知识图谱未刷新 (上次索引: %s)' % last_sync_str)
143
+ print(' 刷新: python .qoder/scripts/git_sync.py --index-only')
144
+ try:
145
+ os.makedirs(os.path.dirname(warned_path), exist_ok=True)
146
+ open(warned_path, 'w').close()
147
+ except OSError:
148
+ pass
149
+ except Exception:
150
+ pass # 新鲜度检查失败不阻塞搜索
151
+
152
+
81
153
  def search_keywords(query, platform=None):
154
+ # 新鲜度检查 (代码改了但索引没更新 → 提示, 不阻塞)
155
+ _check_index_freshness()
156
+
82
157
  # 结果缓存: 同 query+platform+索引未变 → 直接返回缓存输出
83
158
  cache_key = _search_cache_key(query, platform)
84
159
  cache_path = os.path.join(BASE, '.qoder', '.runtime', 'search-cache-%s.txt' % cache_key)
@@ -114,6 +189,53 @@ def search_keywords(query, platform=None):
114
189
  pass
115
190
 
116
191
 
192
+ def _expand_chinese_query(query):
193
+ """把中文查询拆成英文搜索词。
194
+
195
+ 三级匹配:
196
+ 1. 精确匹配: "异常" 在 CN_MAP → 直接扩展
197
+ 2. 分词匹配: "异常记录" → 拆成 "异常" + "记录", 各自扩展
198
+ (贪心最长匹配: 从长到短扫 CN_MAP 的 key)
199
+ 3. 兜底: 拆不出来的部分, 原样小写 (让英文子串索引做匹配)
200
+
201
+ 同时合并 BUSINESS_PATH_MAP 的路径模式。
202
+ """
203
+ words = []
204
+ # 合并 CN_MAP + PRD 自动扩展
205
+ try:
206
+ from common.terms import get_cn_map_with_auto
207
+ cn_map = get_cn_map_with_auto()
208
+ except ImportError:
209
+ cn_map = CN_MAP
210
+
211
+ # 1. 精确匹配
212
+ if query in cn_map:
213
+ words.extend(cn_map[query].split())
214
+ if query in BUSINESS_PATH_MAP:
215
+ words.extend(p.strip('/-').lower() for p in BUSINESS_PATH_MAP[query])
216
+
217
+ # 2. 分词匹配 (查询比单个 CN_MAP key 长 → 需要拆词)
218
+ if not words and len(query) >= 3:
219
+ # 贪心最长匹配: 把 CN_MAP keys 按长度降序排, 逐个从 query 里找
220
+ remaining = query
221
+ matched_segments = []
222
+ for cn in sorted(cn_map.keys(), key=len, reverse=True):
223
+ if cn in remaining:
224
+ matched_segments.append(cn)
225
+ remaining = remaining.replace(cn, ' ')
226
+ if matched_segments:
227
+ for seg in matched_segments:
228
+ words.extend(cn_map[seg].split())
229
+ if seg in BUSINESS_PATH_MAP:
230
+ words.extend(p.strip('/-').lower() for p in BUSINESS_PATH_MAP[seg])
231
+ # 剩余的英文/数字片段也加进去 (兜底)
232
+ for frag in remaining.split():
233
+ if frag and frag.isascii():
234
+ words.append(frag.lower())
235
+
236
+ return words
237
+
238
+
117
239
  def _search_keywords_impl(query, platform=None):
118
240
  """实际的搜索逻辑 (原 search_keywords 内容)。"""
119
241
  ki = load_index('keyword-index.json', hint='Run: python git_sync.py')
@@ -121,11 +243,7 @@ def _search_keywords_impl(query, platform=None):
121
243
  return
122
244
 
123
245
  # Expand Chinese query via the shared term maps (same maps used at build time)
124
- words = []
125
- if query in CN_MAP:
126
- words.extend(CN_MAP[query].split())
127
- if query in BUSINESS_PATH_MAP:
128
- words.extend(p.strip('/-').lower() for p in BUSINESS_PATH_MAP[query])
246
+ words = _expand_chinese_query(query)
129
247
  if words:
130
248
  words = sorted(set(w.lower() for w in words))
131
249
  print('Chinese: {} -> English: {}'.format(query, ' '.join(words)))