@hupan56/wlkj 3.1.32 → 3.3.0
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 +117 -0
- package/package.json +1 -1
- package/templates/qoder/agents/insight-planning.md +67 -67
- package/templates/qoder/agents/prd-reference.md +47 -47
- package/templates/qoder/commands/optional/wl-insight.md +4 -4
- package/templates/qoder/commands/optional/wl-report.md +1 -1
- package/templates/qoder/commands/optional/wl-spec.md +23 -3
- package/templates/qoder/commands/optional/wl-status.md +12 -1
- package/templates/qoder/commands/wl-code.md +138 -7
- package/templates/qoder/commands/wl-commit.md +12 -1
- package/templates/qoder/commands/wl-design.md +70 -6
- package/templates/qoder/commands/wl-init.md +27 -0
- package/templates/qoder/commands/wl-prd.md +230 -15
- package/templates/qoder/commands/wl-req.md +10 -3
- package/templates/qoder/commands/wl-search.md +74 -20
- package/templates/qoder/commands/wl-task.md +3 -3
- package/templates/qoder/commands/wl-test.md +17 -2
- package/templates/qoder/contracts/CHANGELOG.md +418 -0
- package/templates/qoder/contracts/README.md +180 -0
- package/templates/qoder/contracts/code.md +82 -0
- package/templates/qoder/contracts/commit.md +86 -0
- package/templates/qoder/contracts/contract-header.md +76 -0
- package/templates/qoder/contracts/design.md +106 -0
- package/templates/qoder/contracts/fallback.md +126 -0
- package/templates/qoder/contracts/isolation.md +119 -0
- package/templates/qoder/contracts/prd.md +118 -0
- package/templates/qoder/contracts/schemas/design-spec.schema.json +46 -0
- package/templates/qoder/contracts/schemas/prd.schema.json +36 -0
- package/templates/qoder/contracts/schemas/test-cases.schema.json +40 -0
- package/templates/qoder/contracts/spec.md +116 -0
- package/templates/qoder/contracts/task.md +125 -0
- package/templates/qoder/contracts/test.md +112 -0
- package/templates/qoder/hooks/post-tool-use.py +61 -0
- package/templates/qoder/hooks/session-start.py +34 -66
- package/templates/qoder/hooks/stop-eval.py +47 -0
- package/templates/qoder/rules/wl-pipeline.md +37 -0
- package/templates/qoder/scripts/capability/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/__pycache__/registry.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/__pycache__/registry_mcp.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__init__.py +1 -1
- package/templates/qoder/scripts/capability/adapters/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__pycache__/cli.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__pycache__/mcp.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__pycache__/qw.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/mcp.py +76 -100
- package/templates/qoder/scripts/capability/adapters/qw.py +295 -295
- package/templates/qoder/scripts/capability/caps/__init__.py +1 -1
- package/templates/qoder/scripts/capability/caps/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/context.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/cron.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/identity.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/memory.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/notify.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/present.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/repo.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/sandbox.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/memory.py +1 -1
- package/templates/qoder/scripts/capability/registry.py +21 -23
- package/templates/qoder/scripts/capability/registry_mcp.py +69 -5
- package/templates/qoder/scripts/capability/smoke_test_report.json +34 -20
- package/templates/qoder/scripts/deployment/setup/carriers.py +3 -1
- package/templates/qoder/scripts/deployment/setup/init_doctor.py +10 -3
- package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +11 -0
- package/templates/qoder/scripts/deployment/setup/wlkj_shim.py +104 -0
- package/templates/qoder/scripts/domain/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/deployment/deploy_to_test.py +298 -0
- package/templates/qoder/scripts/domain/integration/__init__.py +0 -0
- package/templates/qoder/scripts/domain/integration/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/integration/__pycache__/return_to_platform.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/integration/return_to_platform.py +392 -0
- package/templates/qoder/scripts/domain/integration/spec_upload.py +209 -0
- package/templates/qoder/scripts/domain/kg/build/kg_build.py +268 -25
- package/templates/qoder/scripts/domain/kg/build/kg_incremental.py +108 -3
- package/templates/qoder/scripts/domain/kg/build/kg_signatures.py +169 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +10 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +57 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +62 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +146 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +64 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +52 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +104 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +55 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +95 -0
- package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +8 -0
- package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +22 -0
- package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +55 -0
- package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +90 -0
- package/templates/qoder/scripts/domain/kg/extract/extract.py +84 -0
- package/templates/qoder/scripts/domain/kg/extract/extract.py.bak +430 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +9 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +206 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +132 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +80 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +59 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +93 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +421 -0
- package/templates/qoder/scripts/domain/kg/extract/java/__init__.py +15 -0
- package/templates/qoder/scripts/domain/kg/extract/java/_parser.py +271 -0
- package/templates/qoder/scripts/domain/kg/extract/java/all.py +145 -0
- package/templates/qoder/scripts/domain/kg/extract/java/build_java_to_pg.py +102 -0
- package/templates/qoder/scripts/domain/kg/extract/java/call_chain.py +49 -0
- package/templates/qoder/scripts/domain/kg/extract/java/class_extractor.py +141 -0
- package/templates/qoder/scripts/domain/kg/extract/java/domain_extractor.py +148 -0
- package/templates/qoder/scripts/domain/kg/extract/java/dubbo_extractor.py +33 -0
- package/templates/qoder/scripts/domain/kg/extract/java/endpoint_extractor.py +36 -0
- package/templates/qoder/scripts/domain/kg/extract/java/javadoc_extractor.py +110 -0
- package/templates/qoder/scripts/domain/kg/extract/java/llm_cn_filler.py +150 -0
- package/templates/qoder/scripts/domain/kg/extract/java/member_extractor.py +157 -0
- package/templates/qoder/scripts/domain/kg/extract/java/mybatisplus_extractor.py +34 -0
- package/templates/qoder/scripts/domain/kg/extract/java/pg_upsert.py +165 -0
- package/templates/qoder/scripts/domain/kg/extract/java/satoken_extractor.py +30 -0
- package/templates/qoder/scripts/domain/kg/extract/java/spring_extractor.py +39 -0
- package/templates/qoder/scripts/domain/kg/extract/java/validation_extractor.py +33 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +9 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +79 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +99 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +69 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +78 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +8 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +105 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +153 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +120 -0
- package/templates/qoder/scripts/domain/kg/extract/ts_extract.py +111 -0
- package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +4 -2
- package/templates/qoder/scripts/domain/kg/kg.py +42 -5
- package/templates/qoder/scripts/domain/kg/search/_remote.py +187 -0
- package/templates/qoder/scripts/domain/kg/search/context_pack.py +32 -2
- package/templates/qoder/scripts/domain/kg/search/search_index.py +74 -20
- package/templates/qoder/scripts/domain/kg/storage/kg_duckdb.py +43 -0
- package/templates/qoder/scripts/domain/kg/switch_project.py +159 -0
- package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
- package/templates/qoder/scripts/domain/requirement/req.py +134 -28
- package/templates/qoder/scripts/domain/task/__pycache__/wlkj_panel.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/task/wlkj_panel.py +1348 -0
- package/templates/qoder/scripts/engine/poller.py +219 -0
- package/templates/qoder/scripts/foundation/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/core/paths.py +102 -0
- package/templates/qoder/scripts/foundation/integrations/active_task.py +2 -1
- package/templates/qoder/scripts/orchestration/wlkj.py +4 -0
- package/templates/qoder/scripts/protocol/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +24 -11
- package/templates/qoder/scripts/protocol/transports/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/base.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/cli.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/http.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/stdio.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/http.py +7 -1
- package/templates/qoder/scripts/validation/eval/qwork_harness.py +1 -1
- package/templates/qoder/scripts/validation/eval/report-commands.md +2 -2
- package/templates/qoder/settings.json +27 -9
- package/templates/qoder/skills/design-import/SKILL.md +3 -3
- package/templates/qoder/skills/design-review/SKILL.md +1 -1
- package/templates/qoder/skills/prd-generator/SKILL.md +4 -4
- package/templates/qoder/skills/prd-review/SKILL.md +1 -1
- package/templates/qoder/skills/prototype-generator/SKILL.md +3 -3
- package/templates/qoder/skills/spec-coder/SKILL.md +1 -1
- package/templates/qoder/skills/spec-generator/SKILL.md +80 -23
- package/templates/qoder/skills/test-generator/SKILL.md +1 -1
- package/templates/qoder/skills/wl-code/SKILL.md +13 -1
- package/templates/qoder/skills/wl-commit/SKILL.md +1 -1
- package/templates/qoder/skills/wl-design/SKILL.md +6 -6
- package/templates/qoder/skills/wl-init/SKILL.md +2 -2
- package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
- package/templates/qoder/skills/wl-prd/SKILL.md +60 -0
- package/templates/qoder/skills/wl-report/SKILL.md +2 -2
- package/templates/qoder/skills/wl-search/SKILL.md +1 -1
- package/templates/qoder/skills/wl-spec/SKILL.md +2 -2
- package/templates/qoder/skills/wl-status/SKILL.md +2 -2
- package/templates/qoder/skills/wl-task/SKILL.md +3 -3
- package/templates/qoder/skills/wl-test/SKILL.md +2 -2
- package/templates/qoder/templates/spec-template.md +124 -0
- package/templates/root/AGENTS.md +41 -14
- package/templates/qoder/scripts/domain/task/zentao_panel.py +0 -451
- package/templates/qoder/skills/wl-prd-full/SKILL.md +0 -121
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +0 -50
- package/templates/qoder/skills/wl-prd-review/SKILL.md +0 -47
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Shared extraction utilities for the QODER knowledge graph.
|
|
4
|
+
|
|
5
|
+
Centralizes regex-based extraction that was previously copy-pasted across
|
|
6
|
+
build_style_index.py and git_sync.py. The headline feature is the **call-chain
|
|
7
|
+
extractor**: given a project's source tree, it builds a table mapping each
|
|
8
|
+
clickable UI element (button @click + label) to the backend API endpoint it
|
|
9
|
+
triggers, so test automation and impact analysis can answer:
|
|
10
|
+
|
|
11
|
+
- "点击'新增'按钮会调哪个接口?" (test automation / QoderWork)
|
|
12
|
+
- "改这个接口会影响哪些页面/按钮?" (dev impact analysis)
|
|
13
|
+
|
|
14
|
+
Two passes:
|
|
15
|
+
1. ``build_api_fn_table`` — scan ``src/api/**/*.ts``, parse ``enum Api``
|
|
16
|
+
+ ``export function`` → {fnName: {url, verb, file}}. ~100% reliable.
|
|
17
|
+
2. ``trace_clicks`` — scan ``src/views/**/*.vue``, resolve each
|
|
18
|
+
``@click="handleX"`` to an api function via 4 patterns (direct / drawer
|
|
19
|
+
onConfirm / Modal.confirm onOk / router.push deferred).
|
|
20
|
+
|
|
21
|
+
Confidence is tagged per edge (high/medium/low/deferred) so consumers know
|
|
22
|
+
which traces are statically certain vs need runtime confirmation.
|
|
23
|
+
"""
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ── Reusable extraction regexes (previously duplicated) ──────────────
|
|
29
|
+
|
|
30
|
+
# Table column: title + field (VxeGrid/Vben schema). Used by build_style_index.
|
|
31
|
+
RE_TABLE_COL_TF = re.compile(
|
|
32
|
+
r"title:\s*['\"]([^'\"]+)['\"]\s*,\s*field:\s*['\"]([^'\"]+)['\"]")
|
|
33
|
+
RE_TABLE_COL_FT = re.compile(
|
|
34
|
+
r"field:\s*['\"]([^'\"]+)['\"]\s*,\s*title:\s*['\"]([^'\"]+)['\"]")
|
|
35
|
+
|
|
36
|
+
# Form field: fieldName + label
|
|
37
|
+
RE_FORM_FIELD = re.compile(
|
|
38
|
+
r"fieldName:\s*['\"]([^'\"]+)['\"]\s*,\s*label:\s*['\"]([^'\"]+)['\"]")
|
|
39
|
+
|
|
40
|
+
# Ant Design Vue component imports
|
|
41
|
+
RE_ANT_IMPORTS = re.compile(
|
|
42
|
+
r"import\s+\{([^}]+)\}\s+from\s+['\"]ant-design-vue['\"]")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ── Call-chain extraction ────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
# enum Api { key = '/url', ... } — the URL constant block
|
|
48
|
+
RE_ENUM_API = re.compile(r"enum\s+Api\s*\{([^}]*)\}", re.S)
|
|
49
|
+
RE_ENUM_ENTRY = re.compile(r"(\w+)\s*=\s*['\"`]([^'\"`]+)['\"`]")
|
|
50
|
+
|
|
51
|
+
# export function fnName(...) { return requestClient.<verb>(<urlExpr>, ...) }
|
|
52
|
+
# urlExpr can be Api.key (symbolic) or a template literal `${Api.key}/...`
|
|
53
|
+
# export function fnName(...) { ... }
|
|
54
|
+
# Also matches: export const fnName = (async)? (...) => { (arrow function consts,
|
|
55
|
+
# used for commonExport wrappers like `export const exportFactoryList = async (p) => ...`)
|
|
56
|
+
RE_EXPORT_FN = re.compile(
|
|
57
|
+
r"(?:export\s+(?:async\s+)?function\s+(\w+)\s*\(|"
|
|
58
|
+
r"export\s+const\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{?)", re.M)
|
|
59
|
+
RE_REQUEST_CALL = re.compile(
|
|
60
|
+
r"requestClient\.(\w+)(?:<[^)]*?>)?\s*\(\s*(Api\.\w+|`[^`]*`|'[^']*'|\"[^\"]*\")")
|
|
61
|
+
|
|
62
|
+
# commonExport(Api.key, ...) / commonDownloadExcel(fn, ...) — wrapper that
|
|
63
|
+
# delegates to requestClient. commonExport takes a URL; commonDownloadExcel
|
|
64
|
+
# takes an api *function* as first arg.
|
|
65
|
+
RE_COMMON_EXPORT = re.compile(r"commonExport\s*\(\s*(Api\.\w+)")
|
|
66
|
+
RE_COMMON_DOWNLOAD = re.compile(r"commonDownloadExcel\s*\(\s*(\w+)")
|
|
67
|
+
|
|
68
|
+
# Vue template: @click="handler" or @click.stop="handler(...)"
|
|
69
|
+
RE_CLICK = re.compile(r"@click(?:\.\w+)?\s*=\s*['\"]([^'\"]+)['\"]")
|
|
70
|
+
|
|
71
|
+
# Button text between tags (after @click line, take nearest text node)
|
|
72
|
+
# We grab the text right after the clickable element opening tag.
|
|
73
|
+
RE_BTN_TEXT_AFTER_CLICK = re.compile(
|
|
74
|
+
r"@click(?:\.\w+)?\s*=\s*['\"][^'\"]*['\"][^>]*>\s*([^\n<]{1,20})", re.S)
|
|
75
|
+
|
|
76
|
+
# import { fn1, fn2 } from '#/api/...' — api imports in a Vue SFC
|
|
77
|
+
RE_API_IMPORT = re.compile(
|
|
78
|
+
r"import\s+\{([^}]+)\}\s+from\s+['\"](?:#|@)/(?:api|@/api)/([^'\"]+)['\"]")
|
|
79
|
+
|
|
80
|
+
# connectedComponent: localImport (drawer/modal wiring)
|
|
81
|
+
RE_CONNECTED_COMP = re.compile(r"connectedComponent:\s*(\w+)")
|
|
82
|
+
|
|
83
|
+
# import localName from './xxx.vue' (local default import for drawer)
|
|
84
|
+
RE_LOCAL_VUE_IMPORT = re.compile(
|
|
85
|
+
r"import\s+(\w+)\s+from\s+['\"]([^'\"]+\.vue)['\"]")
|
|
86
|
+
|
|
87
|
+
# function handlerName(...) { ... } — capture full function body (greedy to next function/export/</script)
|
|
88
|
+
RE_FN_BODY = re.compile(
|
|
89
|
+
r"(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*\{", re.M)
|
|
90
|
+
|
|
91
|
+
# modalApi.open() / drawerApi.open() — drawer trigger
|
|
92
|
+
RE_DRAWER_OPEN = re.compile(r"(\w*[Aa]pi)\??\.open\s*\(\s*\)")
|
|
93
|
+
|
|
94
|
+
# Modal.confirm({ onOk: ... }) — delete confirmation
|
|
95
|
+
RE_MODAL_CONFIRM = re.compile(r"Modal\.confirm\s*\(\s*\{", re.S)
|
|
96
|
+
|
|
97
|
+
# useVbenDrawer({ connectedComponent: X }) — drawer declaration
|
|
98
|
+
RE_USE_DRAWER = re.compile(
|
|
99
|
+
r"useVbenDrawer\s*\(\s*\{[^}]*connectedComponent:\s*(\w+)", re.S)
|
|
100
|
+
|
|
101
|
+
# router.push({ path: '...' })
|
|
102
|
+
RE_ROUTER_PUSH = re.compile(r"router\.push\s*\(")
|
|
103
|
+
|
|
104
|
+
# Pattern G: proxyConfig.ajax.query 函数 — ICS VxeGrid 表格数据加载入口
|
|
105
|
+
# 真实写法 (审核 601 个文件确认):
|
|
106
|
+
# proxyConfig: { ajax: { query: async ({page}, formValues) => {
|
|
107
|
+
# return await getUnconventionalDetailList({...}) ← 导入的 api 函数
|
|
108
|
+
# }}}
|
|
109
|
+
# 这不是 proxyConfig.ajax({url}) 内联 url, 而是在 query 函数体里调用已导入的 api 函数。
|
|
110
|
+
# 提取 query 函数体, 交给 _find_api_calls_in_text 匹配。
|
|
111
|
+
RE_PROXY_AJAX = re.compile(
|
|
112
|
+
r"proxyConfig\s*:\s*\{\s*ajax\s*:\s*\{\s*"
|
|
113
|
+
r"(?:query|queryAll|queryPage)\s*:\s*async\s*\(", re.S)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def parse_api_enum_block(content):
|
|
117
|
+
"""Extract {enumKey: '/url'} from an ``enum Api { ... }`` block.
|
|
118
|
+
|
|
119
|
+
Handles enum values containing template literals with ${VAR} — the nested
|
|
120
|
+
braces would break a naive [^}]* regex, so we use brace-depth matching.
|
|
121
|
+
"""
|
|
122
|
+
urls = {}
|
|
123
|
+
# Find "enum Api {" then manually match braces to find the closing }
|
|
124
|
+
for m in re.finditer(r'enum\s+Api\s*\{', content):
|
|
125
|
+
start = m.end()
|
|
126
|
+
depth = 1
|
|
127
|
+
i = start
|
|
128
|
+
while i < len(content) and depth > 0:
|
|
129
|
+
if content[i] == '{':
|
|
130
|
+
depth += 1
|
|
131
|
+
elif content[i] == '}':
|
|
132
|
+
depth -= 1
|
|
133
|
+
i += 1
|
|
134
|
+
block = content[start:i-1] if depth == 0 else content[start:]
|
|
135
|
+
for em in RE_ENUM_ENTRY.finditer(block):
|
|
136
|
+
urls[em.group(1)] = em.group(2)
|
|
137
|
+
return urls
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def build_api_fn_table(api_dir):
|
|
141
|
+
"""Scan ``src/api/**/*.ts`` and build {fnName: {url, verb, file}}.
|
|
142
|
+
|
|
143
|
+
Resolves symbolic ``Api.key`` references via the file's ``enum Api`` block.
|
|
144
|
+
Handles ``commonExport(Api.key)`` wrappers. Returns dict + the enum map
|
|
145
|
+
per file (for cross-file resolution).
|
|
146
|
+
"""
|
|
147
|
+
table = {}
|
|
148
|
+
if not api_dir or not os.path.isdir(api_dir):
|
|
149
|
+
return table
|
|
150
|
+
|
|
151
|
+
for root, dirs, files in os.walk(api_dir):
|
|
152
|
+
dirs[:] = [d for d in dirs if d not in ('node_modules', 'dist', '__pycache__')]
|
|
153
|
+
for f in files:
|
|
154
|
+
if not f.endswith('.ts'):
|
|
155
|
+
continue
|
|
156
|
+
fpath = os.path.join(root, f)
|
|
157
|
+
try:
|
|
158
|
+
content = open(fpath, encoding='utf-8', errors='ignore').read()
|
|
159
|
+
except Exception:
|
|
160
|
+
continue
|
|
161
|
+
urls = parse_api_enum_block(content)
|
|
162
|
+
|
|
163
|
+
# For each exported function, find requestClient call inside its
|
|
164
|
+
# EXACT body (brace-matched, not a fixed-length snippet — a snippet
|
|
165
|
+
# would bleed into the next function and mis-resolve the Api key).
|
|
166
|
+
for fm in RE_EXPORT_FN.finditer(content):
|
|
167
|
+
fn_name = fm.group(1) or fm.group(2) # function name or const name
|
|
168
|
+
if not fn_name:
|
|
169
|
+
continue
|
|
170
|
+
body = _get_fn_body(content, fn_name, max_len=500)
|
|
171
|
+
if not body:
|
|
172
|
+
continue
|
|
173
|
+
entry = None
|
|
174
|
+
# Direct requestClient.<verb>(Api.key or 'literal')
|
|
175
|
+
rm = RE_REQUEST_CALL.search(body)
|
|
176
|
+
if rm:
|
|
177
|
+
verb = rm.group(1)
|
|
178
|
+
url_expr = rm.group(2)
|
|
179
|
+
url = _resolve_url_expr(url_expr, urls)
|
|
180
|
+
if url:
|
|
181
|
+
entry = {'url': url, 'verb': verb, 'file': fpath.replace('\\', '/')}
|
|
182
|
+
# commonExport(Api.key) wrapper
|
|
183
|
+
if not entry:
|
|
184
|
+
cm = RE_COMMON_EXPORT.search(body)
|
|
185
|
+
if cm:
|
|
186
|
+
url = _resolve_url_expr(cm.group(1), urls)
|
|
187
|
+
if url:
|
|
188
|
+
entry = {'url': url, 'verb': 'post', 'file': fpath.replace('\\', '/')}
|
|
189
|
+
if entry:
|
|
190
|
+
table[fn_name] = entry
|
|
191
|
+
return table
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _resolve_url_expr(expr, urls):
|
|
195
|
+
"""Resolve ``Api.key`` or template literal to a concrete URL string."""
|
|
196
|
+
expr = expr.strip()
|
|
197
|
+
if expr.startswith('Api.'):
|
|
198
|
+
key = expr[4:]
|
|
199
|
+
return urls.get(key)
|
|
200
|
+
# Template literal `${Api.key}/xxx` or plain string
|
|
201
|
+
if expr.startswith('`') or expr.startswith("'") or expr.startswith('"'):
|
|
202
|
+
inner = expr.strip('`\'"')
|
|
203
|
+
# Replace ${Api.key} placeholders
|
|
204
|
+
def repl(m):
|
|
205
|
+
return urls.get(m.group(1), m.group(0))
|
|
206
|
+
inner = re.sub(r"\$\{Api\.(\w+)\}", repl, inner)
|
|
207
|
+
# If still has Api. references, try direct
|
|
208
|
+
inner = re.sub(r"Api\.(\w+)", lambda m: urls.get(m.group(1), m.group(0)), inner)
|
|
209
|
+
return inner if '/' in inner else None
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _get_fn_body(content, fn_name, max_len=800):
|
|
214
|
+
"""Get the body text of a named function (best-effort, ~max_len chars).
|
|
215
|
+
|
|
216
|
+
Handles both ``function fnName() {`` and ``const fnName = () => {`` forms.
|
|
217
|
+
"""
|
|
218
|
+
# Try: function fnName( OR const fnName = (async) (...) => {
|
|
219
|
+
for pat in [
|
|
220
|
+
re.compile(r"(?:async\s+)?function\s+" + re.escape(fn_name) + r"\s*\([^)]*\)\s*\{"),
|
|
221
|
+
re.compile(r"const\s+" + re.escape(fn_name) + r"\s*=\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{?"),
|
|
222
|
+
]:
|
|
223
|
+
m = pat.search(content)
|
|
224
|
+
if m:
|
|
225
|
+
break
|
|
226
|
+
else:
|
|
227
|
+
return ''
|
|
228
|
+
start = m.end()
|
|
229
|
+
# If arrow fn without explicit {, grab the expression line instead
|
|
230
|
+
if '=>' in m.group(0) and '{' not in m.group(0):
|
|
231
|
+
return content[start:start + max_len]
|
|
232
|
+
# Simple brace matching
|
|
233
|
+
depth = 1
|
|
234
|
+
i = start
|
|
235
|
+
end = min(len(content), start + max_len)
|
|
236
|
+
while i < end and depth > 0:
|
|
237
|
+
if content[i] == '{':
|
|
238
|
+
depth += 1
|
|
239
|
+
elif content[i] == '}':
|
|
240
|
+
depth -= 1
|
|
241
|
+
i += 1
|
|
242
|
+
return content[start:i]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _find_api_calls_in_text(text, api_table, api_imports):
|
|
246
|
+
"""Return list of (api_fn, endpoint, verb) found in a text snippet."""
|
|
247
|
+
hits = []
|
|
248
|
+
for fn in api_imports:
|
|
249
|
+
if fn in api_table and re.search(r'\b' + re.escape(fn) + r'\b', text):
|
|
250
|
+
info = api_table[fn]
|
|
251
|
+
hits.append((fn, info['url'], info.get('verb', '?')))
|
|
252
|
+
# commonDownloadExcel(fnName, ...) — first arg is an api function
|
|
253
|
+
for dm in RE_COMMON_DOWNLOAD.finditer(text):
|
|
254
|
+
fn = dm.group(1)
|
|
255
|
+
if fn in api_table:
|
|
256
|
+
info = api_table[fn]
|
|
257
|
+
hits.append((fn, info['url'], info.get('verb', '?')))
|
|
258
|
+
return hits
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _resolve_drawer_api(content, drawer_var):
|
|
262
|
+
"""Given a drawer/modal api var name, find its connectedComponent import path."""
|
|
263
|
+
# useVbenDrawer({ connectedComponent: CompName }) declared as [CompName, drawerVar]
|
|
264
|
+
# Pattern: const [CompName, drawerVar] = useVbenDrawer({ connectedComponent: compImport })
|
|
265
|
+
pat = re.compile(
|
|
266
|
+
r"const\s+\[\s*(\w+)\s*,\s*" + re.escape(drawer_var) + r"\s*\]\s*=\s*useVbenDrawer",
|
|
267
|
+
re.S)
|
|
268
|
+
m = pat.search(content)
|
|
269
|
+
if not m:
|
|
270
|
+
# Fallback: find connectedComponent near the drawer var
|
|
271
|
+
for cm in RE_CONNECTED_COMP.finditer(content):
|
|
272
|
+
comp = cm.group(1)
|
|
273
|
+
# Verify this comp's drawer var matches by proximity (within 200 chars)
|
|
274
|
+
if drawer_var in content[max(0, cm.start()-200):cm.start()+200]:
|
|
275
|
+
m = type('x', (), {'group': lambda self, i: comp})()
|
|
276
|
+
break
|
|
277
|
+
if not m:
|
|
278
|
+
return None
|
|
279
|
+
comp_name = m.group(1) if hasattr(m, 'group') else None
|
|
280
|
+
if not comp_name:
|
|
281
|
+
return None
|
|
282
|
+
# Find import localName from './xxx.vue' matching comp_name
|
|
283
|
+
for im in RE_LOCAL_VUE_IMPORT.finditer(content):
|
|
284
|
+
if im.group(1) == comp_name:
|
|
285
|
+
return im.group(2)
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def trace_clicks(vue_path, vue_content, api_table):
|
|
290
|
+
"""Extract click→api traces from a single Vue SFC.
|
|
291
|
+
|
|
292
|
+
Returns list of trace dicts:
|
|
293
|
+
{vue_file, button, handler, api_fn, endpoint, verb, confidence}
|
|
294
|
+
confidence: 'high' (direct), 'medium' (drawer 1-hop), 'low' (modal confirm),
|
|
295
|
+
'deferred' (router.push).
|
|
296
|
+
"""
|
|
297
|
+
traces = []
|
|
298
|
+
dirname = os.path.dirname(vue_path)
|
|
299
|
+
|
|
300
|
+
# 1. Collect api imports in this SFC
|
|
301
|
+
api_imports = set()
|
|
302
|
+
for im in RE_API_IMPORT.finditer(vue_content):
|
|
303
|
+
for name in im.group(1).split(','):
|
|
304
|
+
name = name.strip().split(' as ')[0].strip()
|
|
305
|
+
if name and name in api_table:
|
|
306
|
+
api_imports.add(name)
|
|
307
|
+
|
|
308
|
+
# 2. Find all @click handlers + nearest button text
|
|
309
|
+
clicks = list(RE_CLICK.finditer(vue_content))
|
|
310
|
+
if not clicks:
|
|
311
|
+
return traces
|
|
312
|
+
|
|
313
|
+
# Build drawer map: drawerVar → connected component .vue path.
|
|
314
|
+
# Pattern: const [Comp, drawerVar] = useVbenDrawer({ connectedComponent: compImport })
|
|
315
|
+
# Note: Comp name may differ from compImport (e.g. [FactoryModal, modalApi] with
|
|
316
|
+
# connectedComponent: factoryModal). So we link drawerVar → compImport → import path.
|
|
317
|
+
drawer_map = {}
|
|
318
|
+
# Find all useVbenDrawer declarations and capture the drawer var + connectedComponent
|
|
319
|
+
for dm in re.finditer(
|
|
320
|
+
r"const\s+\[\s*\w+\s*,\s*(\w+)\s*\]\s*=\s*useVbenDrawer\s*\(\s*\{([^}]*)\}",
|
|
321
|
+
vue_content, re.S):
|
|
322
|
+
drawer_var = dm.group(1)
|
|
323
|
+
decl_body = dm.group(2)
|
|
324
|
+
cm = re.search(r"connectedComponent:\s*(\w+)", decl_body)
|
|
325
|
+
if not cm:
|
|
326
|
+
continue
|
|
327
|
+
comp_import_name = cm.group(1)
|
|
328
|
+
# Resolve comp_import_name → './xxx.vue' via local imports
|
|
329
|
+
for im in RE_LOCAL_VUE_IMPORT.finditer(vue_content):
|
|
330
|
+
if im.group(1) == comp_import_name:
|
|
331
|
+
drawer_map[drawer_var] = im.group(2)
|
|
332
|
+
break
|
|
333
|
+
|
|
334
|
+
# 3. For each click, resolve to api
|
|
335
|
+
for i, cm in enumerate(clicks):
|
|
336
|
+
handler_expr = cm.group(1).strip()
|
|
337
|
+
# Button text: text right after the element, or previous text node
|
|
338
|
+
btn_text = ''
|
|
339
|
+
tm = RE_BTN_TEXT_AFTER_CLICK.search(vue_content, cm.start())
|
|
340
|
+
if tm:
|
|
341
|
+
btn_text = tm.group(1).strip()[:12]
|
|
342
|
+
# Strip Vue interpolation {{ }} from button text (e.g. "{{ item.name }}")
|
|
343
|
+
btn_text = re.sub(r'\{\{[^}]*\}\}', '', btn_text).strip()[:12]
|
|
344
|
+
|
|
345
|
+
# Skip inline expressions that aren't function calls:
|
|
346
|
+
# $emit, item.remove(), splice, push — pure frontend, no API
|
|
347
|
+
if handler_expr.startswith('$') or '.splice(' in handler_expr or '.push(' in handler_expr:
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
# Get handler function name (strip args/calls)
|
|
351
|
+
handler_name = handler_expr.split('(')[0].split('.')[0].strip()
|
|
352
|
+
if not handler_name or handler_name in ('undefined', '', 'true', 'false'):
|
|
353
|
+
continue
|
|
354
|
+
# Skip single-word inline handlers (add, remove, item, emit) that are
|
|
355
|
+
# likely data/methods shorthand, not named handler functions
|
|
356
|
+
if handler_name in ('item', 'emit', 'row', 'data') and '.' not in handler_expr:
|
|
357
|
+
continue
|
|
358
|
+
|
|
359
|
+
fn_body = _get_fn_body(vue_content, handler_name)
|
|
360
|
+
|
|
361
|
+
trace = {
|
|
362
|
+
'vue_file': vue_path.replace('\\', '/'),
|
|
363
|
+
'button': btn_text,
|
|
364
|
+
'handler': handler_name,
|
|
365
|
+
'api_fn': None, 'endpoint': None, 'verb': None,
|
|
366
|
+
'confidence': None,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
# Pattern B: direct api call in handler body
|
|
370
|
+
hits = _find_api_calls_in_text(fn_body, api_table, api_imports)
|
|
371
|
+
if hits:
|
|
372
|
+
fn, url, verb = hits[0]
|
|
373
|
+
trace.update(api_fn=fn, endpoint=url, verb=verb, confidence='high')
|
|
374
|
+
traces.append(trace)
|
|
375
|
+
continue
|
|
376
|
+
|
|
377
|
+
# Pattern D: drawer/modal open → resolve connected component
|
|
378
|
+
open_m = RE_DRAWER_OPEN.search(fn_body)
|
|
379
|
+
if open_m:
|
|
380
|
+
drawer_var = open_m.group(1)
|
|
381
|
+
comp_rel = drawer_map.get(drawer_var)
|
|
382
|
+
if comp_rel:
|
|
383
|
+
comp_path = os.path.normpath(os.path.join(dirname, comp_rel))
|
|
384
|
+
if os.path.isfile(comp_path):
|
|
385
|
+
try:
|
|
386
|
+
comp_content = open(comp_path, encoding='utf-8', errors='ignore').read()
|
|
387
|
+
comp_api_imports = set()
|
|
388
|
+
for im in RE_API_IMPORT.finditer(comp_content):
|
|
389
|
+
for name in im.group(1).split(','):
|
|
390
|
+
name = name.strip().split(' as ')[0].strip()
|
|
391
|
+
if name in api_table:
|
|
392
|
+
comp_api_imports.add(name)
|
|
393
|
+
# Find onConfirm/handleConfirm body in the component
|
|
394
|
+
for confirm_name in ('handleConfirm', 'onConfirm', 'handleSubmit', 'onOk'):
|
|
395
|
+
cbody = _get_fn_body(comp_content, confirm_name)
|
|
396
|
+
if cbody:
|
|
397
|
+
chits = _find_api_calls_in_text(cbody, api_table, comp_api_imports)
|
|
398
|
+
if chits:
|
|
399
|
+
fn, url, verb = chits[0]
|
|
400
|
+
trace.update(api_fn=fn, endpoint=url, verb=verb,
|
|
401
|
+
confidence='medium')
|
|
402
|
+
break
|
|
403
|
+
except Exception:
|
|
404
|
+
pass
|
|
405
|
+
if trace['endpoint']:
|
|
406
|
+
traces.append(trace)
|
|
407
|
+
continue
|
|
408
|
+
|
|
409
|
+
# Modal.confirm({ onOk }) pattern
|
|
410
|
+
mc_m = RE_MODAL_CONFIRM.search(fn_body)
|
|
411
|
+
if mc_m:
|
|
412
|
+
onok_body = fn_body[mc_m.end():]
|
|
413
|
+
hits = _find_api_calls_in_text(onok_body[:400], api_table, api_imports)
|
|
414
|
+
if hits:
|
|
415
|
+
fn, url, verb = hits[0]
|
|
416
|
+
trace.update(api_fn=fn, endpoint=url, verb=verb, confidence='low')
|
|
417
|
+
traces.append(trace)
|
|
418
|
+
continue
|
|
419
|
+
|
|
420
|
+
# router.push — deferred
|
|
421
|
+
if RE_ROUTER_PUSH.search(fn_body):
|
|
422
|
+
trace['confidence'] = 'deferred'
|
|
423
|
+
traces.append(trace)
|
|
424
|
+
continue
|
|
425
|
+
|
|
426
|
+
# Unresolved — record as low confidence (handler exists but no api found)
|
|
427
|
+
trace['confidence'] = 'unresolved'
|
|
428
|
+
traces.append(trace)
|
|
429
|
+
|
|
430
|
+
return traces
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""D2: 补社区摘要 + 摘要向量。
|
|
3
|
+
|
|
4
|
+
对 graph_communities 表里 summary 为空的社区:
|
|
5
|
+
① 读 member_ids → 取成员的 canonical+cn → LLM 生成摘要
|
|
6
|
+
② 对摘要算 embedding → summary_vec
|
|
7
|
+
|
|
8
|
+
独立于现有 summarize_communities (不改动它), 直接补 PG。
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import json
|
|
14
|
+
import time
|
|
15
|
+
import urllib.request
|
|
16
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
17
|
+
|
|
18
|
+
_THIS = os.path.dirname(os.path.abspath(__file__))
|
|
19
|
+
for _i in range(8):
|
|
20
|
+
_p = os.path.dirname(_THIS)
|
|
21
|
+
if os.path.isfile(os.path.join(_p, 'foundation', 'bootstrap.py')):
|
|
22
|
+
sys.path.insert(0, _p); break
|
|
23
|
+
_THIS = _p
|
|
24
|
+
|
|
25
|
+
_SUMMARY_PROMPT = """你是软件架构师。下面是一个代码社区(聚类在一起的相关功能)的成员列表。
|
|
26
|
+
请用1-2句话概括这个社区负责什么业务功能。
|
|
27
|
+
|
|
28
|
+
成员列表(类名/端点/按钮, 括号内是中文含义):
|
|
29
|
+
%s
|
|
30
|
+
|
|
31
|
+
只返回摘要文本, 不要解释。"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _call_qwen(prompt: str, timeout: int = 20) -> str:
|
|
35
|
+
key = os.environ.get('DASHSCOPE_API_KEY', '')
|
|
36
|
+
base = os.environ.get('ALI_CHAT_BASE', '').rstrip('/')
|
|
37
|
+
model = os.environ.get('ALI_CHAT_MODEL', 'qwen-plus')
|
|
38
|
+
if not key or not base:
|
|
39
|
+
return ''
|
|
40
|
+
req = urllib.request.Request(
|
|
41
|
+
base + '/chat/completions',
|
|
42
|
+
data=json.dumps({'model': model, 'messages': [{'role': 'user', 'content': prompt}],
|
|
43
|
+
'max_tokens': 200, 'temperature': 0.2}).encode('utf-8'),
|
|
44
|
+
headers={'Authorization': 'Bearer ' + key, 'Content-Type': 'application/json'},
|
|
45
|
+
)
|
|
46
|
+
try:
|
|
47
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
48
|
+
return json.loads(resp.read())['choices'][0]['message']['content'].strip()
|
|
49
|
+
except Exception:
|
|
50
|
+
return ''
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _embed(text: str) -> list:
|
|
54
|
+
"""调 DashScope embedding, 返回 1024 维向量。"""
|
|
55
|
+
key = os.environ.get('DASHSCOPE_API_KEY', '')
|
|
56
|
+
if not key or not text:
|
|
57
|
+
return []
|
|
58
|
+
req = urllib.request.Request(
|
|
59
|
+
'https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings',
|
|
60
|
+
data=json.dumps({'model': 'text-embedding-v3', 'input': [text[:500]], 'dimensions': 1024}).encode('utf-8'),
|
|
61
|
+
headers={'Authorization': 'Bearer ' + key, 'Content-Type': 'application/json'},
|
|
62
|
+
)
|
|
63
|
+
try:
|
|
64
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
65
|
+
return json.loads(resp.read())['data'][0]['embedding']
|
|
66
|
+
except Exception:
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def summarize_missing(limit: int = 2500, do_embed: bool = True, logger=print):
|
|
71
|
+
"""补全缺摘要的社区。
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
limit: 上限 (默认 2500, 覆盖全部 2206 个缺的)
|
|
75
|
+
do_embed: True 同时补 summary_vec
|
|
76
|
+
"""
|
|
77
|
+
from sqlalchemy import text
|
|
78
|
+
from domain.kg.extract.java import pg_upsert
|
|
79
|
+
|
|
80
|
+
engine, _ = pg_upsert._get_pg_engine()
|
|
81
|
+
if engine is None:
|
|
82
|
+
return {'error': 'PG 不可用'}
|
|
83
|
+
|
|
84
|
+
# 取缺摘要的社区
|
|
85
|
+
with engine.connect() as conn:
|
|
86
|
+
rows = conn.execute(text("""
|
|
87
|
+
SELECT id, level, size, member_ids FROM graph_communities
|
|
88
|
+
WHERE summary IS NULL OR summary = ''
|
|
89
|
+
LIMIT :lim
|
|
90
|
+
"""), {'lim': limit}).all()
|
|
91
|
+
logger(' [d2] %d 个社区待补摘要' % len(rows))
|
|
92
|
+
if not rows:
|
|
93
|
+
return {'summarized': 0}
|
|
94
|
+
|
|
95
|
+
# 批量取成员的 canonical+cn (一次查)
|
|
96
|
+
all_member_ids = set()
|
|
97
|
+
for r in rows:
|
|
98
|
+
try:
|
|
99
|
+
mids = json.loads(r[3]) if isinstance(r[3], str) else (r[3] or [])
|
|
100
|
+
all_member_ids.update(mids[:20]) # 每社区最多取20个成员
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
member_info = {} # {entity_id: "canonical(cn)"}
|
|
105
|
+
if all_member_ids:
|
|
106
|
+
member_list = list(all_member_ids)[:5000] # 限 5000 避免查询太大
|
|
107
|
+
# 分 chunk 查
|
|
108
|
+
for ci in range(0, len(member_list), 200):
|
|
109
|
+
chunk = member_list[ci:ci+200]
|
|
110
|
+
arr = "ARRAY[" + ','.join("'" + eid.replace("'", "''") + "'" for eid in chunk) + "]"
|
|
111
|
+
with engine.connect() as conn:
|
|
112
|
+
mrows = conn.execute(text(
|
|
113
|
+
f"SELECT id, canonical, cn FROM entities WHERE id = ANY({arr})"
|
|
114
|
+
)).all()
|
|
115
|
+
for eid, canonical, cn in mrows:
|
|
116
|
+
desc = canonical or eid
|
|
117
|
+
if cn:
|
|
118
|
+
desc += '(' + cn[:20] + ')'
|
|
119
|
+
member_info[eid] = desc[:50]
|
|
120
|
+
|
|
121
|
+
logger(' [d2] 成员信息: %d 条' % len(member_info))
|
|
122
|
+
|
|
123
|
+
# 批量生成摘要 (4线程并发)
|
|
124
|
+
def make_summary(row):
|
|
125
|
+
cid, level, size, member_ids_raw = row
|
|
126
|
+
try:
|
|
127
|
+
mids = json.loads(member_ids_raw) if isinstance(member_ids_raw, str) else (member_ids_raw or [])
|
|
128
|
+
except Exception:
|
|
129
|
+
mids = []
|
|
130
|
+
members_desc = [member_info.get(mid, mid) for mid in mids[:15]]
|
|
131
|
+
if not members_desc:
|
|
132
|
+
members_desc = [mid for mid in mids[:15]]
|
|
133
|
+
if not members_desc:
|
|
134
|
+
return (cid, '空社区')
|
|
135
|
+
prompt = _SUMMARY_PROMPT % '\n'.join(members_desc)
|
|
136
|
+
summary = _call_qwen(prompt)
|
|
137
|
+
if not summary:
|
|
138
|
+
# 兜底: 实体名拼接
|
|
139
|
+
summary = '、'.join(members_desc[:5]) + '等相关功能'
|
|
140
|
+
return (cid, summary[:200])
|
|
141
|
+
|
|
142
|
+
# 并发跑
|
|
143
|
+
t0 = time.time()
|
|
144
|
+
batches = [rows[i:i+10] for i in range(0, len(rows), 10)]
|
|
145
|
+
all_summaries = []
|
|
146
|
+
for bi in range(0, len(batches), 5):
|
|
147
|
+
chunk = batches[bi:bi+5]
|
|
148
|
+
with ThreadPoolExecutor(max_workers=4) as ex:
|
|
149
|
+
results = list(ex.map(make_summary, [r for b in chunk for r in b]))
|
|
150
|
+
all_summaries += results
|
|
151
|
+
if (bi//5+1) % 10 == 0:
|
|
152
|
+
logger(' [d2] %d/%d 社区摘要生成 (%d)' % (len(all_summaries), len(rows), len(all_summaries)))
|
|
153
|
+
|
|
154
|
+
logger(' [d2] 摘要生成完成: %d / %.1fs' % (len(all_summaries), time.time()-t0))
|
|
155
|
+
|
|
156
|
+
# 写 PG (分块提交)
|
|
157
|
+
ok = 0
|
|
158
|
+
vec_ok = 0
|
|
159
|
+
for i in range(0, len(all_summaries), 100):
|
|
160
|
+
chunk = all_summaries[i:i+100]
|
|
161
|
+
with engine.begin() as conn:
|
|
162
|
+
for cid, summary in chunk:
|
|
163
|
+
try:
|
|
164
|
+
# 算摘要向量 (可选)
|
|
165
|
+
vec_str = ''
|
|
166
|
+
if do_embed:
|
|
167
|
+
vec = _embed(summary)
|
|
168
|
+
if vec:
|
|
169
|
+
# summary_vec 存成 JSONB (兼容无 pgvector)
|
|
170
|
+
vec_str = json.dumps(vec)
|
|
171
|
+
if vec_str:
|
|
172
|
+
# summary_vec 是 ARRAY 类型, 用 FLOAT[] 写入
|
|
173
|
+
arr_str = '{' + ','.join(str(float(v)) for v in vec[:1024]) + '}'
|
|
174
|
+
conn.execute(text("""
|
|
175
|
+
UPDATE graph_communities SET summary=:s, summary_vec=CAST(:arr AS FLOAT[])
|
|
176
|
+
WHERE id=:id
|
|
177
|
+
"""), {'s': summary, 'arr': arr_str, 'id': cid})
|
|
178
|
+
vec_ok += 1
|
|
179
|
+
else:
|
|
180
|
+
conn.execute(text("""
|
|
181
|
+
UPDATE graph_communities SET summary=:s WHERE id=:id
|
|
182
|
+
"""), {'s': summary, 'id': cid})
|
|
183
|
+
ok += 1
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
if (i//100+1) % 5 == 0:
|
|
187
|
+
logger(' [d2] 写入 %d/%d (vec=%d)' % (ok, len(all_summaries), vec_ok))
|
|
188
|
+
|
|
189
|
+
dt = time.time() - t0
|
|
190
|
+
logger(' [d2] 完成: %d 摘要 / %d 向量 / %.1fs' % (ok, vec_ok, dt))
|
|
191
|
+
return {'summarized': ok, 'vectorized': vec_ok, 'seconds': round(dt, 1)}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if __name__ == '__main__':
|
|
195
|
+
import argparse
|
|
196
|
+
ap = argparse.ArgumentParser()
|
|
197
|
+
ap.add_argument('--limit', type=int, default=2500)
|
|
198
|
+
ap.add_argument('--no-embed', action='store_true')
|
|
199
|
+
args = ap.parse_args()
|
|
200
|
+
try:
|
|
201
|
+
from dotenv import load_dotenv
|
|
202
|
+
for p in ['wlinkj-workspace/backend/.env', '.env']:
|
|
203
|
+
if os.path.isfile(p): load_dotenv(p); break
|
|
204
|
+
except: pass
|
|
205
|
+
r = summarize_missing(limit=args.limit, do_embed=not args.no_embed)
|
|
206
|
+
print(json.dumps(r, ensure_ascii=False, indent=2))
|