@hupan56/wlkj 3.0.0 → 3.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.
- package/bin/cli.js +1435 -1373
- package/package.json +1 -1
- package/templates/qoder/commands/optional/wl-insight.md +6 -4
- package/templates/qoder/commands/optional/wl-report.md +20 -39
- package/templates/qoder/commands/optional/wl-status.md +4 -4
- package/templates/qoder/commands/wl-code.md +2 -2
- package/templates/qoder/commands/wl-design.md +5 -5
- package/templates/qoder/commands/wl-init.md +3 -3
- package/templates/qoder/commands/wl-prd.md +26 -13
- package/templates/qoder/commands/wl-req.md +3 -3
- package/templates/qoder/commands/wl-search.md +78 -47
- package/templates/qoder/commands/wl-task.md +343 -40
- package/templates/qoder/commands/wl-test.md +10 -10
- package/templates/qoder/config.yaml +35 -13
- package/templates/qoder/hooks/post-tool-use.py +181 -0
- package/templates/qoder/hooks/session-start.py +82 -1
- package/templates/qoder/hooks/stop-eval.py +207 -0
- package/templates/qoder/hooks/user-prompt-submit.py +139 -0
- package/templates/qoder/rules/wl-pipeline.md +70 -8
- package/templates/qoder/scripts/README.md +78 -115
- package/templates/qoder/scripts/capability/__init__.py +26 -0
- package/templates/qoder/scripts/capability/__main__.py +72 -0
- package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
- package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
- package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
- package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
- package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
- package/templates/qoder/scripts/capability/caps/context.py +36 -0
- package/templates/qoder/scripts/capability/caps/cron.py +50 -0
- package/templates/qoder/scripts/capability/caps/identity.py +43 -0
- package/templates/qoder/scripts/capability/caps/memory.py +71 -0
- package/templates/qoder/scripts/capability/caps/notify.py +41 -0
- package/templates/qoder/scripts/capability/caps/present.py +48 -0
- package/templates/qoder/scripts/capability/caps/repo.py +31 -0
- package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
- package/templates/qoder/scripts/capability/chain.py +92 -0
- package/templates/qoder/scripts/capability/memory_chain.py +41 -0
- package/templates/qoder/scripts/capability/registry.py +246 -0
- package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
- package/templates/qoder/scripts/capability/smoke_test.py +211 -0
- package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
- package/templates/qoder/scripts/{report → deployment/setup}/__init__.py +11 -11
- package/templates/qoder/scripts/{setup → deployment/setup}/carriers.py +33 -26
- package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
- package/templates/qoder/scripts/{setup → deployment/setup}/init_doctor.py +40 -16
- package/templates/qoder/scripts/{setup → deployment/setup}/install_qoderwork.py +20 -12
- package/templates/qoder/scripts/{setup → deployment/setup}/platform_doctor.py +8 -2
- package/templates/qoder/scripts/{setup → deployment/setup}/repo_root.py +113 -112
- package/templates/qoder/scripts/{setup → deployment/setup}/setup.py +31 -11
- package/templates/qoder/scripts/{setup → deployment/setup}/setup_lanhu.py +13 -7
- package/templates/qoder/scripts/domain/__init__.py +0 -0
- package/templates/qoder/scripts/domain/design/__init__.py +0 -0
- package/templates/qoder/scripts/{report → domain/design}/fill_prototype.py +12 -6
- package/templates/qoder/scripts/{report → domain/design}/gen_design_doc.py +8 -2
- package/templates/qoder/scripts/{setup → domain/kg}/__init__.py +11 -11
- package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_entity_registry.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_relations.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_style_index.py +13 -7
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_workflows.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build.py +76 -9
- package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build_db.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/kg_incremental.py +11 -6
- package/templates/qoder/scripts/{report → domain/kg/build}/learn_aggregate.py +9 -4
- package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
- package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
- package/templates/qoder/scripts/{common → domain/kg/extract}/test_extract.py +115 -115
- package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
- package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
- package/templates/qoder/scripts/{kg → domain/kg/graph}/kg_link_db.py +7 -2
- package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
- package/templates/qoder/scripts/{kg → domain/kg}/kg.py +263 -30
- package/templates/qoder/scripts/{common → domain/kg}/kg_capabilities.py +182 -182
- package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
- package/templates/qoder/scripts/{kg → domain/kg/search}/context_pack.py +30 -9
- package/templates/qoder/scripts/{kg → domain/kg/search}/enrich_prompt.py +10 -4
- package/templates/qoder/scripts/{kg → domain/kg/search}/prefetch.py +33 -8
- package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
- package/templates/qoder/scripts/{kg → domain/kg/search}/search_index.py +143 -77
- package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
- package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
- package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
- package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
- package/templates/qoder/scripts/{kg → domain/kg/storage}/kg_duckdb.py +38 -14
- package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
- package/templates/qoder/scripts/{report → domain/learning}/learn.py +8 -3
- package/templates/qoder/scripts/{mcp → domain/report}/__init__.py +11 -11
- package/templates/qoder/scripts/{report → domain/report}/add_session.py +9 -3
- package/templates/qoder/scripts/{report → domain/report}/export.py +10 -5
- package/templates/qoder/scripts/{report → domain/report}/report.py +9 -4
- package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
- package/templates/qoder/scripts/{report → domain/report}/role.py +10 -4
- package/templates/qoder/scripts/{report → domain/report}/status.py +112 -9
- package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
- package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
- package/templates/qoder/scripts/{report → domain/requirement}/archive_prd.py +8 -2
- package/templates/qoder/scripts/{report → domain/requirement}/req.py +228 -222
- package/templates/qoder/scripts/{kg → domain/task}/__init__.py +11 -11
- package/templates/qoder/scripts/{task → domain/task}/git_sync.py +40 -21
- package/templates/qoder/scripts/{task → domain/task}/syncgate.py +13 -8
- package/templates/qoder/scripts/{task → domain/task}/task.py +13 -5
- package/templates/qoder/scripts/{task → domain/task}/task_lifecycle.py +606 -596
- package/templates/qoder/scripts/{task → domain/task}/task_query.py +162 -161
- package/templates/qoder/scripts/{task → domain/task}/task_relations.py +425 -424
- package/templates/qoder/scripts/{task → domain/task}/team_sync.py +9 -4
- package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
- package/templates/qoder/scripts/foundation/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation}/bootstrap.py +10 -10
- package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/core}/cmd_registry.py +1 -1
- package/templates/qoder/scripts/{common → foundation/core}/config.py +1 -1
- package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -586
- package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/data}/contract.py +2 -2
- package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
- package/templates/qoder/scripts/{common → foundation/data}/result.py +1 -1
- package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -404
- package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/identity}/check_publish.py +103 -98
- package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -239
- package/templates/qoder/scripts/{common → foundation/identity}/guard.py +2 -2
- package/templates/qoder/scripts/{common → foundation/identity}/identity.py +14 -10
- package/templates/qoder/scripts/{common → foundation/identity}/roles.py +1 -1
- package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
- package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +2 -2
- package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
- package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
- package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
- package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
- package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
- package/templates/qoder/scripts/{common → foundation/protocol}/mcp_base.py +1 -1
- package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
- package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
- package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
- package/templates/qoder/scripts/orchestration/__init__.py +0 -0
- package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
- package/templates/qoder/scripts/protocol/__init__.py +0 -0
- package/templates/qoder/scripts/protocol/browser/README.md +207 -0
- package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
- package/templates/qoder/scripts/protocol/browser/config.env +9 -0
- package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
- package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
- package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
- package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
- package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
- package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
- package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
- package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
- package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
- package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
- package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
- package/templates/qoder/scripts/{mcp → protocol/mcp}/lanhu_stdio_wrapper.py +11 -5
- package/templates/qoder/scripts/{mcp → protocol/mcp}/mcp_doctor.py +47 -14
- package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
- package/templates/qoder/scripts/{mcp → protocol/mcp}/mysql_mcp_server.py +181 -67
- package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
- package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
- package/templates/qoder/scripts/protocol/transports/base.py +87 -0
- package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
- package/templates/qoder/scripts/protocol/transports/http.py +141 -0
- package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
- package/templates/qoder/scripts/run_weekly_update.bat +27 -18
- package/templates/qoder/scripts/run_weekly_update.sh +22 -13
- package/templates/qoder/scripts/validation/__init__.py +0 -0
- package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
- package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
- package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
- package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
- package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
- package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
- package/templates/qoder/scripts/{report → validation/metrics}/eval_prd.py +17 -10
- package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
- package/templates/qoder/scripts/validation/test/__init__.py +11 -0
- package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
- package/templates/qoder/scripts/{test → validation/test}/autotest.py +64 -4
- package/templates/qoder/scripts/{common → validation/test}/autotest_auth.py +1 -1
- package/templates/qoder/scripts/{test → validation/test}/autotest_batch.py +16 -3
- package/templates/qoder/scripts/{test → validation/test}/autotest_data.py +17 -12
- package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
- package/templates/qoder/scripts/{test → validation/test}/autotest_run.py +17 -3
- package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
- package/templates/qoder/scripts/{test → validation/test}/benchmark.py +12 -6
- package/templates/qoder/scripts/{test → validation/test}/kg_auto_login.py +8 -2
- package/templates/qoder/scripts/{test → validation/test}/kg_test_runner.py +7 -2
- package/templates/qoder/scripts/{test → validation/test}/page_probe.py +8 -3
- package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
- package/templates/qoder/settings.json +30 -0
- package/templates/qoder/skills/design-import/SKILL.md +22 -13
- package/templates/qoder/skills/design-review/SKILL.md +19 -10
- package/templates/qoder/skills/prd-generator/SKILL.md +22 -18
- package/templates/qoder/skills/prd-review/SKILL.md +6 -4
- package/templates/qoder/skills/prototype-generator/SKILL.md +63 -14
- package/templates/qoder/skills/spec-coder/SKILL.md +22 -13
- package/templates/qoder/skills/spec-generator/SKILL.md +22 -10
- package/templates/qoder/skills/test-generator/SKILL.md +7 -5
- package/templates/qoder/skills/wl-code/SKILL.md +23 -11
- package/templates/qoder/skills/wl-commit/SKILL.md +9 -8
- package/templates/qoder/skills/wl-design/SKILL.md +5 -3
- package/templates/qoder/skills/wl-init/SKILL.md +6 -6
- package/templates/qoder/skills/wl-insight/SKILL.md +23 -10
- package/templates/qoder/skills/wl-prd-full/SKILL.md +57 -6
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +3 -3
- package/templates/qoder/skills/wl-prd-review/SKILL.md +16 -4
- package/templates/qoder/skills/wl-report/SKILL.md +25 -23
- package/templates/qoder/skills/wl-search/SKILL.md +149 -78
- package/templates/qoder/skills/wl-spec/SKILL.md +7 -5
- package/templates/qoder/skills/wl-status/SKILL.md +48 -16
- package/templates/qoder/skills/wl-task/SKILL.md +92 -10
- package/templates/qoder/skills/wl-test/SKILL.md +75 -29
- package/templates/qoder/templates/prd-full-template.md +7 -0
- package/templates/root/AGENTS.md +275 -236
- package/templates/root/requirements.txt +3 -0
- package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
- package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
- package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +0 -23
- package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +0 -23
- package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +0 -1
- package/templates/.qoder/learning/eval-history.jsonl +0 -28
- package/templates/data/index/wiki-index.json +0 -8
- package/templates/qoder/nul +0 -4
- package/templates/qoder/scripts/kg/extract.py +0 -398
- package/templates/qoder/scripts/kg/kg_semantic.py +0 -156
- package/templates/qoder/scripts/mcp/kg_mcp_server.py +0 -606
- package/templates/qoder/scripts/mcp/mcp_launcher.py +0 -442
- package/templates/qoder/scripts/mcp/zentao_mcp_server.py +0 -384
- package/templates/qoder/scripts/task/__init__.py +0 -11
- package/templates/qoder/scripts/test/__init__.py +0 -11
- package/templates/qoder/scripts/wlkj.py +0 -116
- /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
- /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
- /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
- /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
- /package/templates/qoder/scripts/{test → validation/test}/secure-ls.js +0 -0
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
|
|
3
|
+
import os as _o, sys as _s
|
|
4
|
+
_f = _o.path.abspath(__file__)
|
|
5
|
+
for _ in range(10):
|
|
6
|
+
_f = _o.path.dirname(_f)
|
|
7
|
+
_cp = _o.path.join(_f, 'foundation')
|
|
8
|
+
if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
|
|
9
|
+
break
|
|
10
|
+
if _cp not in _s.path: _s.path.insert(0, _cp)
|
|
11
|
+
from bootstrap import setup; setup()
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
LLM semantic labeling layer for the knowledge graph.
|
|
15
|
+
|
|
16
|
+
After kg_build.py extracts structural data (AST-level), this module:
|
|
17
|
+
1. Finds entities that lack semantic labels (cn == dir, no business_domain)
|
|
18
|
+
2. Outputs a labeling prompt for the AI (QoderWork) to fill in
|
|
19
|
+
3. Provides a write-back function to store labels in DuckDB
|
|
20
|
+
|
|
21
|
+
The AI (QoderWork's LLM) sees the prompt during session-start or when
|
|
22
|
+
kg_build reports new modules, and labels them — zero human trigger.
|
|
23
|
+
|
|
24
|
+
Usage (called by AI automatically):
|
|
25
|
+
python kg_semantic.py --check # check what needs labeling
|
|
26
|
+
python kg_semantic.py --label '{"assets": {"cn": "资产管理", "domain": "固定资产"}}'
|
|
27
|
+
"""
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
SCRIPTS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 子包→scripts/根
|
|
34
|
+
sys.path.insert(0, SCRIPTS)
|
|
35
|
+
from foundation.core.paths import DATA_INDEX_DIR
|
|
36
|
+
from domain.kg.storage.kg_duckdb import get_db, get_db_readonly
|
|
37
|
+
|
|
38
|
+
DB_PATH = str(DATA_INDEX_DIR / 'kg.duckdb')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def check_unlabeled():
|
|
42
|
+
"""Find modules that need semantic labeling (cn == dir or cn is empty)."""
|
|
43
|
+
con = get_db()
|
|
44
|
+
# Find FEATURE entities where cn == dir (unlabeled) or cn is empty
|
|
45
|
+
rows = con.execute("""
|
|
46
|
+
SELECT id, canonical, cn FROM entities
|
|
47
|
+
WHERE type = 'FEATURE'
|
|
48
|
+
AND (cn = canonical OR cn = '' OR cn IS NULL)
|
|
49
|
+
ORDER BY canonical
|
|
50
|
+
""").fetchall()
|
|
51
|
+
|
|
52
|
+
if not rows:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
# Build labeling prompt
|
|
56
|
+
modules = []
|
|
57
|
+
for eid, dirname, cn in rows:
|
|
58
|
+
# Gather context: what endpoints/pages does this module have?
|
|
59
|
+
ep_count = con.execute(
|
|
60
|
+
"SELECT COUNT(*) FROM edges WHERE from_id = ? AND edge_type = 'has_endpoint'",
|
|
61
|
+
[eid]
|
|
62
|
+
).fetchone()[0]
|
|
63
|
+
btn_count = con.execute(
|
|
64
|
+
"SELECT COUNT(*) FROM edges WHERE from_id = ? AND edge_type = 'has_button'",
|
|
65
|
+
[eid]
|
|
66
|
+
).fetchone()[0]
|
|
67
|
+
# Sample endpoints for context
|
|
68
|
+
sample_eps = con.execute("""
|
|
69
|
+
SELECT e.to_id FROM edges e
|
|
70
|
+
WHERE e.from_id = ? AND e.edge_type = 'has_endpoint' LIMIT 5
|
|
71
|
+
""", [eid]).fetchall()
|
|
72
|
+
|
|
73
|
+
modules.append({
|
|
74
|
+
'dir': dirname,
|
|
75
|
+
'endpoints': ep_count,
|
|
76
|
+
'buttons': btn_count,
|
|
77
|
+
'sample_apis': [r[0].replace('E:', '') for r in sample_eps],
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
con.close()
|
|
81
|
+
|
|
82
|
+
prompt = {
|
|
83
|
+
'action': 'semantic_labeling_needed',
|
|
84
|
+
'description': '以下模块需要语义标注(中文名+业务领域)。请根据目录名和API端点推断。',
|
|
85
|
+
'modules': modules,
|
|
86
|
+
'instructions': (
|
|
87
|
+
'对每个模块返回: {"目录名": {"cn": "中文名", "domain": "业务领域"}}\n'
|
|
88
|
+
'示例: {"assets": {"cn": "资产管理", "domain": "固定资产管理"}}\n'
|
|
89
|
+
'然后用 kg_semantic.py --label 写回'
|
|
90
|
+
),
|
|
91
|
+
}
|
|
92
|
+
return prompt
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def apply_labels(labels_json):
|
|
96
|
+
"""Write semantic labels back to DuckDB.
|
|
97
|
+
|
|
98
|
+
labels_json: {"assets": {"cn": "资产管理", "domain": "固定资产"}, ...}
|
|
99
|
+
"""
|
|
100
|
+
labels = json.loads(labels_json) if isinstance(labels_json, str) else labels_json
|
|
101
|
+
con = get_db()
|
|
102
|
+
updated = 0
|
|
103
|
+
|
|
104
|
+
for dirname, info in labels.items():
|
|
105
|
+
cn = info.get('cn', dirname)
|
|
106
|
+
domain = info.get('domain', '')
|
|
107
|
+
|
|
108
|
+
# Find the FEATURE entity by canonical name (dir)
|
|
109
|
+
rows = con.execute(
|
|
110
|
+
"SELECT id FROM entities WHERE type='FEATURE' AND canonical = ?",
|
|
111
|
+
[dirname]
|
|
112
|
+
).fetchall()
|
|
113
|
+
|
|
114
|
+
for row in rows:
|
|
115
|
+
eid = row[0]
|
|
116
|
+
# Update cn
|
|
117
|
+
con.execute(
|
|
118
|
+
"UPDATE entities SET cn = ? WHERE id = ?",
|
|
119
|
+
[cn, eid]
|
|
120
|
+
)
|
|
121
|
+
# Update props with domain
|
|
122
|
+
props_row = con.execute(
|
|
123
|
+
"SELECT props FROM entities WHERE id = ?", [eid]
|
|
124
|
+
).fetchone()
|
|
125
|
+
if props_row:
|
|
126
|
+
try:
|
|
127
|
+
props = json.loads(props_row[0]) if props_row[0] else {}
|
|
128
|
+
except Exception:
|
|
129
|
+
props = {}
|
|
130
|
+
props['business_domain'] = domain
|
|
131
|
+
props['labeled'] = True
|
|
132
|
+
con.execute(
|
|
133
|
+
"UPDATE entities SET props = ? WHERE id = ?",
|
|
134
|
+
[json.dumps(props, ensure_ascii=False), eid]
|
|
135
|
+
)
|
|
136
|
+
# Update alias
|
|
137
|
+
con.execute(
|
|
138
|
+
"INSERT OR REPLACE INTO aliases VALUES (?, ?)",
|
|
139
|
+
[cn.lower(), eid]
|
|
140
|
+
)
|
|
141
|
+
updated += 1
|
|
142
|
+
|
|
143
|
+
con.close()
|
|
144
|
+
return updated
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ============================================================
|
|
148
|
+
# 语义检索 (v3.1: bge-small-zh embedding)
|
|
149
|
+
# ============================================================
|
|
150
|
+
# 解决关键词精确匹配的漏召问题: 查"报销"能命中叫 expense/reimburse 的代码。
|
|
151
|
+
# 用 sentence-transformers + BAAI/bge-small-zh-v1.5 (95MB, 512维, 完全离线)。
|
|
152
|
+
#
|
|
153
|
+
# 设计:
|
|
154
|
+
# - build_embeddings(): 遍历 entities 的 canonical+cn 文本, 批量生成向量写入 DuckDB
|
|
155
|
+
# - semantic_search(): query→向量→余弦相似度→top_k
|
|
156
|
+
# - 模型懒加载 + 进程内单例 (避免重复加载 95MB)
|
|
157
|
+
# - 模型未装时优雅降级: 不报错, 返回提示安装
|
|
158
|
+
|
|
159
|
+
_MODEL_NAME = 'BAAI/bge-small-zh-v1.5'
|
|
160
|
+
_MODEL = None # 进程内单例
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _load_model():
|
|
164
|
+
"""懒加载 sentence-transformers 模型 (进程内单例)。
|
|
165
|
+
|
|
166
|
+
临时移除 common/ 等 scripts 子目录的 sys.path, 避免 common/filelock.py
|
|
167
|
+
遮蔽 pip 的 filelock 包 (huggingface_hub 需要 filelock.BaseFileLock)。
|
|
168
|
+
加载完恢复 sys.path。
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
model 实例, 或 None (未安装 sentence-transformers / 加载失败)
|
|
172
|
+
"""
|
|
173
|
+
global _MODEL
|
|
174
|
+
if _MODEL is not None:
|
|
175
|
+
return _MODEL
|
|
176
|
+
# 隔离: 临时移除所有含本地模块的 scripts 子目录, 防止遮蔽第三方包。
|
|
177
|
+
# 关键: common/filelock.py 会遮蔽 pip 的 filelock (huggingface_hub 需要
|
|
178
|
+
# filelock.BaseFileLock)。仅清 sys.path 不够 —— 若 filelock 已被 import 过
|
|
179
|
+
# (本进程早先走过 bootstrap), sys.modules 里缓存的是错的, 要一并清掉。
|
|
180
|
+
saved_paths = [p for p in sys.path]
|
|
181
|
+
scripts_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
182
|
+
sys.path[:] = [p for p in sys.path
|
|
183
|
+
if not (p and p.lower().startswith(scripts_root.lower()))
|
|
184
|
+
and os.path.basename(p) not in ('foundation', 'kg', 'mcp', 'report', 'test', 'setup')]
|
|
185
|
+
# 清掉被本地模块污染的 sys.modules 缓存 (filelock 等), 让第三方重新 import 干净版
|
|
186
|
+
_purged = {}
|
|
187
|
+
for mod_name in list(sys.modules):
|
|
188
|
+
if mod_name == 'filelock' or mod_name.startswith('filelock.'):
|
|
189
|
+
_purged[mod_name] = sys.modules.pop(mod_name)
|
|
190
|
+
try:
|
|
191
|
+
from sentence_transformers import SentenceTransformer
|
|
192
|
+
_MODEL = SentenceTransformer(_MODEL_NAME)
|
|
193
|
+
return _MODEL
|
|
194
|
+
except ImportError:
|
|
195
|
+
return None
|
|
196
|
+
except Exception:
|
|
197
|
+
return None
|
|
198
|
+
finally:
|
|
199
|
+
sys.path[:] = saved_paths # 恢复 sys.path
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def embedding_status():
|
|
203
|
+
"""检查 embedding 能力是否可用 + 已构建多少。"""
|
|
204
|
+
available = _load_model() is not None
|
|
205
|
+
con = get_db()
|
|
206
|
+
try:
|
|
207
|
+
count = con.execute("SELECT COUNT(*) FROM embeddings").fetchone()[0]
|
|
208
|
+
except Exception:
|
|
209
|
+
count = 0
|
|
210
|
+
finally:
|
|
211
|
+
con.close()
|
|
212
|
+
return {
|
|
213
|
+
'model': _MODEL_NAME if available else None,
|
|
214
|
+
'available': available,
|
|
215
|
+
'installed_hint': 'pip install sentence-transformers' if not available else None,
|
|
216
|
+
'embeddings_count': count,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def build_embeddings(batch_size=64):
|
|
221
|
+
"""为 entities 表构建 embedding 向量并写入 DuckDB。
|
|
222
|
+
|
|
223
|
+
只 embed 有业务语义的实体类型 (FEATURE/MODULE/ENDPOINT/BUTTON),
|
|
224
|
+
跳过纯文件路径实体。文本 = canonical + cn (中英拼接, 提升检索召回)。
|
|
225
|
+
|
|
226
|
+
模型未装时直接返回 0, 不报错 (调用方自行提示)。
|
|
227
|
+
"""
|
|
228
|
+
model = _load_model()
|
|
229
|
+
if model is None:
|
|
230
|
+
print('[semantic] sentence-transformers 未安装, 跳过 embedding 构建')
|
|
231
|
+
print(' 安装: pip install sentence-transformers')
|
|
232
|
+
return 0
|
|
233
|
+
|
|
234
|
+
con = get_db()
|
|
235
|
+
try:
|
|
236
|
+
# 确保表存在 (旧 kg.duckdb 可能没有 embeddings 表)
|
|
237
|
+
from domain.kg.storage.kg_duckdb import init_schema
|
|
238
|
+
init_schema(con)
|
|
239
|
+
# 取需要 embed 的实体 (有业务语义的)
|
|
240
|
+
rows = con.execute(
|
|
241
|
+
"SELECT id, canonical, COALESCE(cn, '') FROM entities "
|
|
242
|
+
"WHERE type IN ('FEATURE', 'MODULE', 'ENDPOINT', 'BUTTON') "
|
|
243
|
+
"AND canonical IS NOT NULL AND canonical != ''"
|
|
244
|
+
).fetchall()
|
|
245
|
+
if not rows:
|
|
246
|
+
return 0
|
|
247
|
+
|
|
248
|
+
# 清空旧 embedding
|
|
249
|
+
con.execute("DELETE FROM embeddings")
|
|
250
|
+
|
|
251
|
+
total = 0
|
|
252
|
+
texts = []
|
|
253
|
+
ids = []
|
|
254
|
+
for eid, canonical, cn in rows:
|
|
255
|
+
# 拼接文本: 中文名 + 英文名 (中英都有时检索效果最好)
|
|
256
|
+
text = (cn + ' ' + canonical).strip() if cn else canonical
|
|
257
|
+
texts.append(text)
|
|
258
|
+
ids.append(eid)
|
|
259
|
+
|
|
260
|
+
# 批量 encode
|
|
261
|
+
for i in range(0, len(texts), batch_size):
|
|
262
|
+
batch_texts = texts[i:i + batch_size]
|
|
263
|
+
batch_ids = ids[i:i + batch_size]
|
|
264
|
+
vecs = model.encode(batch_texts, normalize_embeddings=True)
|
|
265
|
+
for eid, text, vec in zip(batch_ids, batch_texts, vecs):
|
|
266
|
+
vec_list = [float(x) for x in vec]
|
|
267
|
+
con.execute(
|
|
268
|
+
"INSERT INTO embeddings (entity_id, text, vec, model) VALUES (?, ?, ?, ?)",
|
|
269
|
+
[eid, text, vec_list, _MODEL_NAME]
|
|
270
|
+
)
|
|
271
|
+
total += len(batch_texts)
|
|
272
|
+
if (i // batch_size) % 10 == 0:
|
|
273
|
+
print(' [semantic] %d/%d...' % (total, len(texts)))
|
|
274
|
+
return total
|
|
275
|
+
finally:
|
|
276
|
+
con.close()
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def semantic_search(query, top_k=10):
|
|
280
|
+
"""语义搜索: query→向量→余弦相似度→top_k 实体。
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
query: 自然语言查询 (中文/英文均可), 如 "报销流程" "车辆调度"
|
|
284
|
+
top_k: 返回前 N 条
|
|
285
|
+
Returns:
|
|
286
|
+
[(entity_id, text, score), ...] 或 None (模型未装/无数据)
|
|
287
|
+
"""
|
|
288
|
+
model = _load_model()
|
|
289
|
+
if model is None:
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
con = get_db_readonly()
|
|
293
|
+
if con is None:
|
|
294
|
+
return None
|
|
295
|
+
try:
|
|
296
|
+
try:
|
|
297
|
+
count = con.execute("SELECT COUNT(*) FROM embeddings").fetchone()[0]
|
|
298
|
+
except Exception:
|
|
299
|
+
return None # embeddings 表不存在 (旧库)
|
|
300
|
+
if count == 0:
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
# query 向量
|
|
304
|
+
q_vec = model.encode([query], normalize_embeddings=True)[0]
|
|
305
|
+
q_list = [float(x) for x in q_vec]
|
|
306
|
+
|
|
307
|
+
# 优先用 DuckDB 原生 list_dot_product (向量已 normalize, 点积=余弦相似度)。
|
|
308
|
+
# 比 Python 循环快 ~23x (553ms → 24ms, 5180 向量)。旧版 DuckDB 无此函数
|
|
309
|
+
# 则回退 Python 循环。
|
|
310
|
+
scored = None
|
|
311
|
+
try:
|
|
312
|
+
q_lit = '[' + ','.join('%.7f' % x for x in q_list) + ']'
|
|
313
|
+
sql_rows = con.execute(
|
|
314
|
+
"SELECT entity_id, text, list_dot_product(vec, " + q_lit + "::FLOAT[]) AS score "
|
|
315
|
+
"FROM embeddings WHERE vec IS NOT NULL ORDER BY score DESC LIMIT ?",
|
|
316
|
+
[top_k]
|
|
317
|
+
).fetchall()
|
|
318
|
+
scored = [(eid, text, float(score)) for eid, text, score in sql_rows]
|
|
319
|
+
except Exception:
|
|
320
|
+
scored = None # 函数不存在/SQL 失败 → 回退 Python
|
|
321
|
+
|
|
322
|
+
if scored is None:
|
|
323
|
+
# 回退: Python 端算余弦相似度 (兼容旧 DuckDB)
|
|
324
|
+
rows = con.execute("SELECT entity_id, text, vec FROM embeddings").fetchall()
|
|
325
|
+
scored = []
|
|
326
|
+
for eid, text, vec in rows:
|
|
327
|
+
if not vec:
|
|
328
|
+
continue
|
|
329
|
+
score = sum(a * b for a, b in zip(q_list, vec))
|
|
330
|
+
scored.append((eid, text, score))
|
|
331
|
+
scored.sort(key=lambda x: -x[2])
|
|
332
|
+
scored = scored[:top_k]
|
|
333
|
+
return scored
|
|
334
|
+
finally:
|
|
335
|
+
con.close()
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def semantic_search_files(query, top_k=10, min_score=0.35):
|
|
339
|
+
"""语义搜索 → 把实体解析回文件路径 (供 search_index/prefetch 接入)。
|
|
340
|
+
|
|
341
|
+
semantic_search 返回的是实体, 但用户要的是"读哪个文件"。这个函数把:
|
|
342
|
+
- ENDPOINT 实体 -> endpoints.controller_file (Java 后端)
|
|
343
|
+
- BUTTON 实体 -> eid 内嵌的 vue 文件路径 (B:{vue}:{handler})
|
|
344
|
+
- FEATURE 实体 -> modules.path (按 project+dir)
|
|
345
|
+
解析成可读文件, 附 cn(中文名) + score。
|
|
346
|
+
|
|
347
|
+
模型未装/无 embedding 时返回 None (调用方静默跳过, 不报错)。
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
[(file_path, entity_id, cn, score), ...] 或 None
|
|
351
|
+
"""
|
|
352
|
+
results = semantic_search(query, top_k=top_k * 2) # 多取些, 解析失败会过滤掉
|
|
353
|
+
if not results:
|
|
354
|
+
return None
|
|
355
|
+
|
|
356
|
+
con = get_db_readonly()
|
|
357
|
+
if con is None:
|
|
358
|
+
# 只读连接失败 (文件不存在) → 至少返回原始实体信息 (无文件解析)
|
|
359
|
+
return [(eid, eid, text, score) for eid, text, score in results[:top_k]]
|
|
360
|
+
try:
|
|
361
|
+
out = []
|
|
362
|
+
seen_files = set() # 去重 (同一文件多个实体只留最高分)
|
|
363
|
+
for eid, text, score in results:
|
|
364
|
+
if score < min_score:
|
|
365
|
+
continue
|
|
366
|
+
etype = eid.split(':', 1)[0] if ':' in eid else ''
|
|
367
|
+
file_path = None
|
|
368
|
+
cn = ''
|
|
369
|
+
|
|
370
|
+
if etype == 'E':
|
|
371
|
+
# ENDPOINT: 查 endpoints 表拿 controller_file
|
|
372
|
+
ep = eid[2:]
|
|
373
|
+
row = con.execute(
|
|
374
|
+
"SELECT controller_file FROM endpoints WHERE endpoint = ?", [ep]
|
|
375
|
+
).fetchone()
|
|
376
|
+
if row and row[0]:
|
|
377
|
+
file_path = row[0]
|
|
378
|
+
|
|
379
|
+
elif etype == 'B':
|
|
380
|
+
# BUTTON: eid 格式 B:{vue_path}:{handler}, vue_path 含 .vue
|
|
381
|
+
parts = eid.split(':')
|
|
382
|
+
if len(parts) >= 2 and '.vue' in parts[1]:
|
|
383
|
+
file_path = parts[1]
|
|
384
|
+
# cn 从 entities 表 (backfill 填的中文名)
|
|
385
|
+
row = con.execute("SELECT cn FROM entities WHERE id = ?", [eid]).fetchone()
|
|
386
|
+
cn = row[0] if row and row[0] else ''
|
|
387
|
+
|
|
388
|
+
elif etype == 'F':
|
|
389
|
+
# FEATURE: F:{project}:{dir} -> modules.path
|
|
390
|
+
fparts = eid.split(':')
|
|
391
|
+
if len(fparts) >= 3:
|
|
392
|
+
proj, mod_dir = fparts[1], fparts[2]
|
|
393
|
+
row = con.execute(
|
|
394
|
+
"SELECT path FROM modules WHERE project = ? AND dir = ?",
|
|
395
|
+
[proj, mod_dir]
|
|
396
|
+
).fetchone()
|
|
397
|
+
if row and row[0]:
|
|
398
|
+
file_path = '%s/%s' % (proj, row[0])
|
|
399
|
+
row2 = con.execute("SELECT cn FROM entities WHERE id = ?", [eid]).fetchone()
|
|
400
|
+
cn = row2[0] if row2 and row2[0] else ''
|
|
401
|
+
|
|
402
|
+
if file_path and file_path not in seen_files:
|
|
403
|
+
seen_files.add(file_path)
|
|
404
|
+
out.append((file_path, eid, cn or text, score))
|
|
405
|
+
|
|
406
|
+
if len(out) >= top_k:
|
|
407
|
+
break
|
|
408
|
+
return out if out else []
|
|
409
|
+
finally:
|
|
410
|
+
con.close()
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def filter_semantic_by_platform(results, platform, repo_base=None):
|
|
414
|
+
"""把 semantic_search_files 的结果按 platform 过滤 + 补全项目前缀。
|
|
415
|
+
|
|
416
|
+
难点: BUTTON 实体的 vue 路径是相对路径 (无项目前缀, 如 hr/timeManage/...),
|
|
417
|
+
无法靠子串判项目。这里用文件实际存在性兜底, 并把相对路径补成可读全路径。
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
results: semantic_search_files 的输出 [(file_path, eid, cn, score), ...]
|
|
421
|
+
platform: 'web'/'app'/None
|
|
422
|
+
repo_base: 仓库根 (用于查文件存在性; 默认按 DATA_INDEX_DIR 推 repo 根)
|
|
423
|
+
Returns:
|
|
424
|
+
[(full_file_path, cn, score), ...] 已过滤 + 前缀补全
|
|
425
|
+
"""
|
|
426
|
+
if not platform or not results:
|
|
427
|
+
# 无 platform 过滤, 只补前缀提示 (返回相对路径)
|
|
428
|
+
return [(fp.replace('\\', '/'), cn, sc) for fp, eid, cn, sc in results]
|
|
429
|
+
# platform -> 项目名 + view 根
|
|
430
|
+
_PF = {'web': 'fywl-ui', 'pc': 'fywl-ui', '管理端': 'fywl-ui',
|
|
431
|
+
'app': 'Carmg-H5', 'mobile': 'Carmg-H5', 'h5': 'Carmg-H5', '移动端': 'Carmg-H5'}
|
|
432
|
+
target = _PF.get(platform.lower(), platform)
|
|
433
|
+
if repo_base is None:
|
|
434
|
+
repo_base = str(DATA_INDEX_DIR.parent.parent) # data/index -> data -> repo
|
|
435
|
+
view_root = None
|
|
436
|
+
if target == 'fywl-ui':
|
|
437
|
+
view_root = os.path.join(repo_base, 'data', 'code', 'fywl-ui', 'apps', 'ics', 'src', 'views')
|
|
438
|
+
elif target == 'Carmg-H5':
|
|
439
|
+
view_root = os.path.join(repo_base, 'data', 'code', 'Carmg-H5', 'src')
|
|
440
|
+
|
|
441
|
+
KNOWN_PROJ = ('fywl-ui', 'fywl-ics', 'Carmg-H5')
|
|
442
|
+
out = []
|
|
443
|
+
for fp, eid, cn, sc in results:
|
|
444
|
+
fp_norm = fp.replace('\\', '/')
|
|
445
|
+
first_seg = fp_norm.split('/')[0]
|
|
446
|
+
if first_seg in KNOWN_PROJ:
|
|
447
|
+
# 明确项目前缀 → 判项目名
|
|
448
|
+
if target.lower() in fp_norm.lower():
|
|
449
|
+
out.append((fp_norm, cn, sc))
|
|
450
|
+
elif view_root and os.path.isfile(os.path.join(view_root, fp_norm)):
|
|
451
|
+
# 相对路径 (BUTTON vue) → 存在性兜底 + 补前缀
|
|
452
|
+
if target == 'fywl-ui':
|
|
453
|
+
out.append(('fywl-ui/apps/ics/src/views/' + fp_norm, cn, sc))
|
|
454
|
+
else:
|
|
455
|
+
out.append((target + '/src/' + fp_norm, cn, sc))
|
|
456
|
+
return out
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
# ============================================================
|
|
460
|
+
# cn 回填 (v3.1.1: 为 BUTTON/ENDPOINT/CONTROLLER 补中文名)
|
|
461
|
+
# ============================================================
|
|
462
|
+
# kg_build 构建 BUTTON 时已尽量填 cn, 但存量库 / props 缺失 / ENDPOINT/CONTROLLER
|
|
463
|
+
# 一直没中文。这个命令对存量实体做一次性回填:
|
|
464
|
+
# - BUTTON: 从 props.button_text 清洗 icon 字体(&#x...; / iconfont 类名) + 噪音
|
|
465
|
+
# - ENDPOINT: 从 path 末段提动词(get/add/update...) + 资源, 启发式翻译
|
|
466
|
+
# - CONTROLLER: CamelCase 拆词, 词典映射已知业务域 (Asset→资产 等)
|
|
467
|
+
# 回填后需重算 embeddings (build-embeddings) 才能让语义检索命中中文。
|
|
468
|
+
|
|
469
|
+
# 动词 → 中文 (覆盖常见 CRUD + 业务动作)
|
|
470
|
+
_VERB_CN = {
|
|
471
|
+
'get': '查询', 'list': '列表', 'page': '分页', 'query': '查询', 'find': '查找',
|
|
472
|
+
'search': '搜索', 'detail': '详情', 'count': '统计', 'info': '信息',
|
|
473
|
+
'add': '新增', 'create': '创建', 'save': '保存', 'insert': '新增', 'new': '新增',
|
|
474
|
+
'edit': '编辑', 'update': '修改', 'modify': '修改', 'set': '设置', 'change': '变更',
|
|
475
|
+
'delete': '删除', 'remove': '移除', 'del': '删除',
|
|
476
|
+
'export': '导出', 'download': '下载', 'import': '导入', 'upload': '上传',
|
|
477
|
+
'submit': '提交', 'commit': '提交', 'apply': '申请', 'approve': '审批', 'reject': '驳回',
|
|
478
|
+
'check': '校验', 'verify': '核实', 'judge': '判定', 'validate': '校验',
|
|
479
|
+
'send': '发送', 'push': '推送', 'notify': '通知', 'mail': '邮件',
|
|
480
|
+
'start': '启动', 'begin': '开始', 'execute': '执行', 'run': '运行', 'finish': '完成', 'complete': '完成',
|
|
481
|
+
'login': '登录', 'logout': '登出', 'register': '注册', 'auth': '鉴权',
|
|
482
|
+
'bind': '绑定', 'unbind': '解绑', 'link': '关联', 'transfer': '转办',
|
|
483
|
+
'generate': '生成', 'parse': '解析', 'convert': '转换', 'sync': '同步',
|
|
484
|
+
'enable': '启用', 'disable': '停用', 'lock': '锁定', 'unlock': '解锁',
|
|
485
|
+
'reset': '重置', 'refresh': '刷新', 'copy': '复制', 'merge': '合并', 'split': '拆分',
|
|
486
|
+
'schedule': '排程', 'clock': '打卡', 'abnormal': '异常',
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
# 业务域 token → 中文 (CamelCase 拆词后逐词映射, 拼接成领域名)
|
|
490
|
+
_DOMAIN_CN = {
|
|
491
|
+
'asset': '资产', 'assets': '资产', 'ledger': '台账', 'inventory': '盘点',
|
|
492
|
+
'vehicle': '车辆', 'car': '车辆', 'motor': '机车', 'model': '车型',
|
|
493
|
+
'maintain': '维护', 'maintenance': '维护', 'repair': '维修', 'fault': '故障',
|
|
494
|
+
'contract': '合同', 'supplier': '供应商', 'purchase': '采购', 'order': '订单',
|
|
495
|
+
'user': '用户', 'org': '组织', 'organization': '组织', 'dept': '部门',
|
|
496
|
+
'role': '角色', 'permission': '权限', 'auth': '权限', 'menu': '菜单',
|
|
497
|
+
'attendance': '考勤', 'clock': '打卡', 'leave': '请假', 'overtime': '加班',
|
|
498
|
+
'salary': '薪资', 'wage': '工资', 'payroll': '工资单',
|
|
499
|
+
'insurance': '保险', 'social': '社保',
|
|
500
|
+
'schedule': '排班', 'shift': '班次', 'plan': '计划', 'task': '任务',
|
|
501
|
+
'employee': '员工', 'staff': '员工', 'personnel': '人事', 'hr': '人事',
|
|
502
|
+
'mail': '邮件', 'message': '消息', 'notify': '通知', 'notice': '公告',
|
|
503
|
+
'file': '文件', 'document': '文档', 'doc': '文档', 'ocr': '识别', 'image': '图片',
|
|
504
|
+
'project': '项目', 'business': '业务', 'category': '分类', 'type': '类型',
|
|
505
|
+
'terminal': '终端', 'device': '设备', 'heartbeat': '心跳', 'redis': '缓存',
|
|
506
|
+
'log': '日志', 'report': '报表', 'export': '导出', 'import': '导入',
|
|
507
|
+
'feedback': '反馈', 'complaint': '投诉', 'evaluate': '评价',
|
|
508
|
+
'ocrasset': '资产识别', 'asssetlist': '资产列表',
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _clean_button_text(text):
|
|
513
|
+
"""清洗按钮文本: 去 icon 字体编码 / iconfont 类名 / 多余空白, 保留中文。"""
|
|
514
|
+
if not text:
|
|
515
|
+
return ''
|
|
516
|
+
# 去 HTML 实体 icon 编码:  📧
|
|
517
|
+
t = re.sub(r'&#x[0-9a-fA-F]+;', '', text)
|
|
518
|
+
# 去 iconfont 类名残留 (如 :icon-plus: / iconfont-plus / icon-foo)
|
|
519
|
+
t = re.sub(r':?\s*icon[a-zA-Z0-9_-]*\s*:?', ' ', t, flags=re.IGNORECASE)
|
|
520
|
+
# 去 <i ...> 标签
|
|
521
|
+
t = re.sub(r'<[^>]+>', '', t)
|
|
522
|
+
# 去英文/数字噪音 token (handler 名混入等), 保留中文与必要符号
|
|
523
|
+
t = re.sub(r'[A-Za-z_][A-Za-z0-9_]*', '', t)
|
|
524
|
+
# 去 Vue 模板表达式残留 ({{ . }} / { var } / ${...})
|
|
525
|
+
t = re.sub(r'\{\{?\s*[^}]*\}?\}', '', t)
|
|
526
|
+
# 合并空白 + 去首尾符号/冒号
|
|
527
|
+
t = re.sub(r'\s+', ' ', t).strip(' >\t\r\n,,;;:-')
|
|
528
|
+
return t
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _endpoint_to_cn(endpoint):
|
|
532
|
+
"""从 endpoint path 启发式翻译成中文标签。
|
|
533
|
+
例: /updateOcrAssetList -> '修改 资产识别列表', /getManufactorList -> '查询 厂商列表'
|
|
534
|
+
失败时返回 '' (不强行造词, 避免污染检索)。"""
|
|
535
|
+
if not endpoint:
|
|
536
|
+
return ''
|
|
537
|
+
# 取最后一段 path (去掉 {id} 等参数段)
|
|
538
|
+
seg = endpoint.rstrip('/').split('/')[-1]
|
|
539
|
+
seg = re.sub(r'\{.*?\}', '', seg).strip()
|
|
540
|
+
if not seg or len(seg) <= 1:
|
|
541
|
+
return ''
|
|
542
|
+
# 拆成词: camelCase 切分 + 斜杠/下划线
|
|
543
|
+
parts = re.findall(r'[A-Z]?[a-z]+|[A-Z]+(?=[A-Z]|$)', seg)
|
|
544
|
+
parts = [p.lower() for p in parts if len(p) >= 2]
|
|
545
|
+
if not parts:
|
|
546
|
+
return ''
|
|
547
|
+
# 第一个动词词 → 中文动作, 其余 → 领域词
|
|
548
|
+
cn_parts = []
|
|
549
|
+
verb_done = False
|
|
550
|
+
for w in parts:
|
|
551
|
+
if not verb_done and w in _VERB_CN:
|
|
552
|
+
cn_parts.append(_VERB_CN[w])
|
|
553
|
+
verb_done = True
|
|
554
|
+
elif w in _DOMAIN_CN:
|
|
555
|
+
cn_parts.append(_DOMAIN_CN[w])
|
|
556
|
+
# 末尾 List/Page/All/Detail/By 后缀
|
|
557
|
+
if parts and parts[-1] == 'list' and '列表' not in cn_parts:
|
|
558
|
+
cn_parts.append('列表')
|
|
559
|
+
elif parts and parts[-1] == 'detail' and '详情' not in cn_parts:
|
|
560
|
+
cn_parts.append('详情')
|
|
561
|
+
return ' '.join(cn_parts) if cn_parts else ''
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _controller_to_cn(name):
|
|
565
|
+
"""从 Controller 类名翻译中文领域名。
|
|
566
|
+
例: AssetGeneralLedgerController -> '资产 台账',
|
|
567
|
+
VehicleModelAnalysisController -> '车辆 车型 分析'."""
|
|
568
|
+
if not name:
|
|
569
|
+
return ''
|
|
570
|
+
base = name.replace('Controller', '').replace('RestController', '')
|
|
571
|
+
# CamelCase 拆词
|
|
572
|
+
parts = re.findall(r'[A-Z]?[a-z]+|[A-Z]+(?=[A-Z]|$)', base)
|
|
573
|
+
parts = [p.lower() for p in parts if len(p) >= 2]
|
|
574
|
+
cn_parts = [_DOMAIN_CN[w] for w in parts if w in _DOMAIN_CN]
|
|
575
|
+
# 去重保序
|
|
576
|
+
seen = set()
|
|
577
|
+
uniq = []
|
|
578
|
+
for c in cn_parts:
|
|
579
|
+
if c not in seen:
|
|
580
|
+
seen.add(c)
|
|
581
|
+
uniq.append(c)
|
|
582
|
+
return ' '.join(uniq) if uniq else ''
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def backfill_cn():
|
|
586
|
+
"""回填 BUTTON/ENDPOINT/CONTROLLER 的 cn 字段 (存量库一次性补全)。
|
|
587
|
+
|
|
588
|
+
只更新 cn 为空的实体 (不覆盖已有中文)。回填后建议重算 embeddings。
|
|
589
|
+
Returns: dict {type: updated_count}
|
|
590
|
+
"""
|
|
591
|
+
con = get_db()
|
|
592
|
+
try:
|
|
593
|
+
stats = {'BUTTON': 0, 'ENDPOINT': 0, 'CONTROLLER': 0}
|
|
594
|
+
|
|
595
|
+
# BUTTON: 从 props.button_text 清洗
|
|
596
|
+
rows = con.execute(
|
|
597
|
+
"SELECT id, props FROM entities WHERE type='BUTTON' "
|
|
598
|
+
"AND (cn = '' OR cn IS NULL)"
|
|
599
|
+
).fetchall()
|
|
600
|
+
for eid, props_json in rows:
|
|
601
|
+
try:
|
|
602
|
+
props = json.loads(props_json) if props_json else {}
|
|
603
|
+
except Exception:
|
|
604
|
+
props = {}
|
|
605
|
+
btn_text = props.get('button_text', '')
|
|
606
|
+
cn = _clean_button_text(btn_text)
|
|
607
|
+
if cn:
|
|
608
|
+
con.execute("UPDATE entities SET cn=? WHERE id=?", [cn, eid])
|
|
609
|
+
con.execute("INSERT OR REPLACE INTO aliases VALUES (?, ?)", [cn.lower(), eid])
|
|
610
|
+
stats['BUTTON'] += 1
|
|
611
|
+
|
|
612
|
+
# ENDPOINT: 启发式翻译 path
|
|
613
|
+
rows = con.execute(
|
|
614
|
+
"SELECT id, canonical FROM entities WHERE type='ENDPOINT' "
|
|
615
|
+
"AND (cn = '' OR cn IS NULL)"
|
|
616
|
+
).fetchall()
|
|
617
|
+
for eid, canonical in rows:
|
|
618
|
+
cn = _endpoint_to_cn(canonical)
|
|
619
|
+
if cn:
|
|
620
|
+
con.execute("UPDATE entities SET cn=? WHERE id=?", [cn, eid])
|
|
621
|
+
con.execute("INSERT OR REPLACE INTO aliases VALUES (?, ?)", [cn.lower(), eid])
|
|
622
|
+
stats['ENDPOINT'] += 1
|
|
623
|
+
|
|
624
|
+
# CONTROLLER: 类名翻译
|
|
625
|
+
rows = con.execute(
|
|
626
|
+
"SELECT id, canonical FROM entities WHERE type='CONTROLLER' "
|
|
627
|
+
"AND (cn = '' OR cn IS NULL)"
|
|
628
|
+
).fetchall()
|
|
629
|
+
for eid, canonical in rows:
|
|
630
|
+
cn = _controller_to_cn(canonical)
|
|
631
|
+
if cn:
|
|
632
|
+
con.execute("UPDATE entities SET cn=? WHERE id=?", [cn, eid])
|
|
633
|
+
con.execute("INSERT OR REPLACE INTO aliases VALUES (?, ?)", [cn.lower(), eid])
|
|
634
|
+
stats['CONTROLLER'] += 1
|
|
635
|
+
|
|
636
|
+
return stats
|
|
637
|
+
finally:
|
|
638
|
+
con.close()
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
if __name__ == '__main__':
|
|
642
|
+
if len(sys.argv) > 1 and sys.argv[1] == '--check':
|
|
643
|
+
result = check_unlabeled()
|
|
644
|
+
if result:
|
|
645
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
646
|
+
else:
|
|
647
|
+
print('{"status": "all_labeled", "message": "所有模块已有语义标注"}')
|
|
648
|
+
|
|
649
|
+
elif len(sys.argv) > 2 and sys.argv[1] == '--label':
|
|
650
|
+
count = apply_labels(sys.argv[2])
|
|
651
|
+
print('Updated %d entities with semantic labels' % count)
|
|
652
|
+
|
|
653
|
+
elif len(sys.argv) > 1 and sys.argv[1] == 'build-embeddings':
|
|
654
|
+
n = build_embeddings()
|
|
655
|
+
print('Built %d embeddings' % n)
|
|
656
|
+
|
|
657
|
+
elif len(sys.argv) > 2 and sys.argv[1] == 'search':
|
|
658
|
+
results = semantic_search(sys.argv[2])
|
|
659
|
+
if results:
|
|
660
|
+
for eid, text, score in results:
|
|
661
|
+
print(' %.3f %s' % (score, text[:70]))
|
|
662
|
+
else:
|
|
663
|
+
print('(无结果或 embedding 未构建)')
|
|
664
|
+
|
|
665
|
+
elif len(sys.argv) > 1 and sys.argv[1] == '--status':
|
|
666
|
+
st = embedding_status()
|
|
667
|
+
print(json.dumps(st, ensure_ascii=False, indent=2))
|
|
668
|
+
|
|
669
|
+
elif len(sys.argv) > 1 and sys.argv[1] == '--backfill-cn':
|
|
670
|
+
stats = backfill_cn()
|
|
671
|
+
print('cn 回填完成:')
|
|
672
|
+
for t, c in stats.items():
|
|
673
|
+
print(' %-12s: %d' % (t, c))
|
|
674
|
+
print('(建议接着跑: python kg_semantic.py build-embeddings)')
|
|
675
|
+
|
|
676
|
+
else:
|
|
677
|
+
print('Usage:')
|
|
678
|
+
print(' python kg_semantic.py --check # 查看待标注模块')
|
|
679
|
+
print(' python kg_semantic.py --label \'{"assets":{"cn":"资产管理","domain":"固定资产"}}\'')
|
|
680
|
+
print(' python kg_semantic.py build-embeddings # 构建 embedding 向量')
|
|
681
|
+
print(' python kg_semantic.py search 报销 # 语义搜索')
|
|
682
|
+
print(' python kg_semantic.py --status # embedding 状态')
|
|
683
|
+
print(' python kg_semantic.py --backfill-cn # 回填 BUTTON/ENDPOINT/CONTROLLER 中文')
|