| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477 |
- const fs = require('fs');
- const path = require('path');
- const { PACKAGE_ROOT, resolveWorkspaceRoot } = require('../core/runtime-context');
- const PROJECT_ROOT = resolveWorkspaceRoot({ packageRoot: PACKAGE_ROOT });
- const PACKAGE_KNOWLEDGE_BASE_DIR = path.join(PACKAGE_ROOT, 'knowledge-base');
- const PROJECT_KNOWLEDGE_BASE_DIR = path.join(PROJECT_ROOT, 'knowledge-base');
- const KNOWLEDGE_BASE_DIR = fs.existsSync(path.join(PROJECT_KNOWLEDGE_BASE_DIR, 'catalog.json'))
- ? PROJECT_KNOWLEDGE_BASE_DIR
- : PACKAGE_KNOWLEDGE_BASE_DIR;
- const CATALOG_FILE = path.join(KNOWLEDGE_BASE_DIR, 'catalog.json');
- const SOURCE_WORKSPACE_ROOT = path.resolve(PACKAGE_ROOT, '..', '..');
- const ALLOWED_EXTENSIONS = new Set(['.md', '.json', '.csv', '.txt', '.js']);
- const MAX_PREVIEW_BYTES = 2 * 1024 * 1024;
- const PAGE_BY_SKILL = {
- 'qiwei-real-estate-auto-reply': 'agent',
- 'qiwei-agent-supervisor': 'agent',
- 'qiwei-group-management': 'groups',
- 'qiwei-customer-ops': 'customer-ops',
- 'qiwei-portrait-tags': 'portraits',
- 'qiwei-customer-transfer': 'transfers',
- 'qiwei-official-meeting': 'knowledge',
- 'qiwei-official-doc': 'knowledge',
- 'qiwei-official-todo': 'knowledge',
- 'qiwei-login': 'status',
- 'qiwei-dashboard': 'status',
- };
- const CAPABILITY_TAGS = {
- 'qiwei-api-catalog': ['100+ API', '接口文档', '通用调用'],
- 'qiwei-broker-playbook': ['顾问策略', '批量生成', '导出'],
- 'qiwei-capability-router': ['能力路由', '双通道', '自动选择'],
- 'qiwei-customer-ops': ['批量加好友', '建群', '欢迎语'],
- 'qiwei-customer-transfer': ['客户交接', '预览', '执行'],
- 'qiwei-dashboard': ['本地工作台', '状态管理'],
- 'qiwei-group-management': ['群识别', '回调接入', '新消息'],
- 'qiwei-login': ['扫码登录', '订阅', '席位'],
- 'qiwei-official-doc': ['文档知识库', 'Markdown', '官方 CLI'],
- 'qiwei-official-meeting': ['会议同步', 'AI 知识沉淀', '官方 CLI'],
- 'qiwei-official-todo': ['待办同步', '任务推进', '官方 CLI'],
- 'qiwei-portrait-tags': ['客户画像', '标签', '批量导出'],
- 'qiwei-real-estate-auto-reply': ['房产 Agent', '需求识别', '房源工具'],
- 'qiwei-agent-supervisor': ['人工监管', '审核', '审计'],
- 'qiwei-voice': ['语音下载', '解码', '转写'],
- 'qiwei-webhook-relay': ['Webhook', 'Relay', '长轮询'],
- };
- function readJson(filePath, fallback = {}) {
- try { return JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '')); }
- catch { return fallback; }
- }
- function packageDefinitions() {
- const definitions = [
- {
- id: 'unified-source',
- label: '统一源码版',
- description: '当前 4320 Dashboard 的主运行包,承载页面、MCP 工具和真实企微 Agent。',
- root: PACKAGE_ROOT,
- status: 'running',
- },
- ...(PROJECT_ROOT.toLowerCase() !== PACKAGE_ROOT.toLowerCase() ? [{
- id: 'customer-project',
- label: '当前客户项目',
- description: '客户项目中的自定义 Skills、知识库和业务扩展。',
- root: PROJECT_ROOT,
- skillsRoot: path.join(PROJECT_ROOT, '.claude', 'skills'),
- status: 'active',
- }] : []),
- {
- id: 'openclaw',
- label: 'OpenClaw 企微技能包',
- description: 'OpenClaw 版本的企微技能与独立 Agent 工具实现。',
- root: path.resolve(SOURCE_WORKSPACE_ROOT, '..', 'openclaw-voc-skill', 'claude-code', 'claude-code-qiwe-assistant'),
- status: 'connected',
- },
- {
- id: 'agent-workbench',
- label: '企微 Agent Workbench',
- description: 'SQLite 状态机、人工审核、暂停、审计和工具循环的来源实现。',
- root: path.join(SOURCE_WORKSPACE_ROOT, 'qiwei-agent-workbench'),
- status: 'merged',
- },
- ];
- const seenRoots = new Set();
- return definitions.filter(item => {
- if (!fs.existsSync(item.root)) return false;
- const root = path.resolve(item.root).toLowerCase();
- if (seenRoots.has(root)) return false;
- seenRoots.add(root);
- return true;
- });
- }
- function loadCatalog() {
- const parsed = readJson(CATALOG_FILE, { version: 1, libraries: [] });
- const libraries = (parsed.libraries || []).map(item => ({
- ...item,
- root: path.resolve(KNOWLEDGE_BASE_DIR, item.path),
- extensions: new Set((item.extensions || []).map(value => String(value).toLowerCase())),
- kind: item.id === 'property-data'
- ? 'property-library'
- : item.id === 'meeting-knowledge'
- ? 'meeting-library'
- : item.id === 'doc-knowledge'
- ? 'doc-library'
- : item.id === 'todo-knowledge'
- ? 'todo-library'
- : item.id === 'task-knowledge'
- ? 'task-library'
- : 'document-library',
- })).filter(item => item.id !== 'property-data' || fs.existsSync(item.root));
- for (const pkg of packageDefinitions()) {
- const skillsRoot = pkg.skillsRoot || path.join(pkg.root, 'skills');
- if (!fs.existsSync(skillsRoot)) continue;
- libraries.push({
- id: `skills-${pkg.id}`,
- label: `${pkg.label} · 技能说明`,
- description: 'SKILL.md 与 references 文档',
- root: skillsRoot,
- extensions: new Set(['.md', '.json', '.txt']),
- kind: 'skill-library',
- });
- }
- return { version: parsed.version || 1, libraries };
- }
- function encodeNodeId(libraryId, relativePath) {
- return `${libraryId}~${Buffer.from(String(relativePath || ''), 'utf8').toString('base64url')}`;
- }
- function decodeNodeId(nodeId) {
- const index = String(nodeId || '').indexOf('~');
- if (index < 1) throw new Error('知识库节点不存在');
- const libraryId = nodeId.slice(0, index);
- const relativePath = Buffer.from(nodeId.slice(index + 1), 'base64url').toString('utf8');
- return { libraryId, relativePath };
- }
- function safeNodePath(nodeId) {
- const { libraryId, relativePath } = decodeNodeId(nodeId);
- const library = loadCatalog().libraries.find(item => item.id === libraryId);
- if (!library) throw new Error('知识库目录不存在');
- const resolved = path.resolve(library.root, relativePath);
- const rootPrefix = `${path.resolve(library.root).toLowerCase()}${path.sep}`;
- if (resolved.toLowerCase() !== path.resolve(library.root).toLowerCase() && !resolved.toLowerCase().startsWith(rootPrefix)) {
- throw new Error('禁止访问知识库目录之外的文件');
- }
- return { library, filePath: resolved, relativePath };
- }
- function fileKind(filePath) {
- const base = path.basename(filePath).toLowerCase();
- const extension = path.extname(filePath).toLowerCase();
- if (base === 'properties.json') return 'property-dataset';
- if (base === 'skill.md') return 'skill-document';
- if (extension === '.md') return 'markdown';
- if (extension === '.json') return 'json';
- if (extension === '.csv') return 'csv';
- if (extension === '.js') return 'code';
- return 'text';
- }
- function buildDirectoryChildren(library, directory, relativeDir = '', depth = 0) {
- if (depth > 7 || !fs.existsSync(directory)) return [];
- const entries = fs.readdirSync(directory, { withFileTypes: true })
- .filter(entry => !entry.name.startsWith('.') && entry.name !== 'node_modules')
- .map(entry => {
- const relativePath = path.join(relativeDir, entry.name);
- const fullPath = path.join(directory, entry.name);
- if (entry.isDirectory()) {
- const children = buildDirectoryChildren(library, fullPath, relativePath, depth + 1);
- if (!children.length) return null;
- return {
- id: encodeNodeId(library.id, relativePath),
- name: entry.name,
- type: 'folder',
- children,
- fileCount: children.reduce((sum, item) => sum + (item.type === 'file' ? 1 : item.fileCount || 0), 0),
- };
- }
- const extension = path.extname(entry.name).toLowerCase();
- if (!ALLOWED_EXTENSIONS.has(extension) || !library.extensions.has(extension)) return null;
- const stats = fs.statSync(fullPath);
- return {
- id: encodeNodeId(library.id, relativePath),
- name: entry.name,
- type: 'file',
- kind: fileKind(fullPath),
- size: stats.size,
- modifiedAt: stats.mtime.toISOString(),
- relativePath: relativePath.replace(/\\/g, '/'),
- };
- })
- .filter(Boolean);
- return entries.sort((a, b) => {
- if (a.type !== b.type) return a.type === 'folder' ? -1 : 1;
- return a.name.localeCompare(b.name, 'zh-CN');
- });
- }
- function propertySourcePath() {
- const library = loadCatalog().libraries.find(item => item.id === 'property-data');
- if (!library) throw new Error('房源数据目录未配置');
- const filePath = path.join(library.root, 'properties.json');
- if (!fs.existsSync(filePath)) throw new Error('properties.json 不存在');
- return filePath;
- }
- function normalizeProperty(raw) {
- return {
- id: String(raw.id || raw.objectId || raw.property_id || ''),
- community: raw.community || raw.community_name || '',
- district: raw.district || raw.area_name || '',
- totalPrice: Number(raw.totalPrice ?? raw.total_price ?? raw.price_total ?? 0),
- unitPrice: Number(raw.unitPrice ?? raw.unit_price ?? raw.price_unit ?? 0),
- layout: raw.layout || raw.house_type || '',
- area: Number(raw.area || 0),
- floor: raw.floor || raw.floor_info || '',
- floorLevel: raw.floorLevel || raw.floor_level || '',
- orientation: raw.orientation || '',
- decoration: raw.decoration || '',
- buildingAge: Number(raw.buildingAge ?? raw.building_age ?? 0),
- isSchoolDistrict: Boolean(raw.isSchoolDistrict ?? raw.is_school_district),
- schoolName: raw.schoolName || raw.school_name || '',
- parking: raw.parking || '',
- surrounding: raw.surrounding || '',
- ownerSituation: raw.ownerSituation || raw.owner_situation || '',
- priceDropSpace: Number(raw.priceDropSpace ?? raw.price_drop_space ?? 0),
- isFiveYearOnly: Boolean(raw.isFiveYearOnly ?? raw.is_five_year_only),
- highlights: raw.highlightTags || raw.highlight_tags || raw.tags || [],
- scores: {
- community: Number(raw.communityQuality || 0),
- transport: Number(raw.transportScore || 0),
- surrounding: Number(raw.surroundingScore || 0),
- priceAdvantage: Number(raw.priceAdvantage || 0),
- },
- };
- }
- function allProperties() {
- const parsed = readJson(propertySourcePath(), {});
- const rows = Array.isArray(parsed) ? parsed : (parsed.properties || parsed.data || []);
- return rows.map(normalizeProperty).filter(item => item.id || item.community);
- }
- function propertyStats(properties) {
- const prices = properties.map(item => item.totalPrice).filter(value => value > 0);
- return {
- total: properties.length,
- districts: [...new Set(properties.map(item => item.district.split('-')[0]).filter(Boolean))].sort(),
- layouts: [...new Set(properties.map(item => item.layout).filter(Boolean))].sort(),
- decorations: [...new Set(properties.map(item => item.decoration).filter(Boolean))].sort(),
- minPrice: prices.length ? Math.min(...prices) : 0,
- maxPrice: prices.length ? Math.max(...prices) : 0,
- averagePrice: prices.length ? Math.round(prices.reduce((sum, value) => sum + value, 0) / prices.length) : 0,
- schoolDistrictCount: properties.filter(item => item.isSchoolDistrict).length,
- };
- }
- function listKnowledgeTree() {
- const catalog = loadCatalog();
- const roots = catalog.libraries.map(library => {
- const fileChildren = buildDirectoryChildren(library, library.root);
- const virtualNode = library.kind === 'meeting-library'
- ? { relativePath: '__meeting_dashboard__', name: '会议工作台', kind: 'meeting-dashboard' }
- : library.kind === 'doc-library'
- ? { relativePath: '__doc_dashboard__', name: '文档工作台', kind: 'doc-dashboard' }
- : library.kind === 'todo-library'
- ? { relativePath: '__todo_dashboard__', name: '待办中心', kind: 'todo-dashboard' }
- : library.kind === 'task-library'
- ? { relativePath: '__task_dashboard__', name: '统一任务工作台', kind: 'task-dashboard' }
- : null;
- const children = virtualNode
- ? [{ id: encodeNodeId(library.id, virtualNode.relativePath), name: virtualNode.name, type: 'file', kind: virtualNode.kind, virtual: true }, ...fileChildren]
- : fileChildren;
- return {
- id: encodeNodeId(library.id, ''),
- libraryId: library.id,
- name: library.label,
- description: library.description,
- type: 'folder',
- kind: library.kind,
- path: library.root,
- children,
- fileCount: fileChildren.reduce((sum, item) => sum + (item.type === 'file' ? 1 : item.fileCount || 0), 0),
- };
- });
- const properties = allProperties();
- return {
- status: 'ok',
- data: {
- catalogVersion: catalog.version,
- roots,
- summary: {
- libraryCount: roots.length,
- fileCount: roots.reduce((sum, item) => sum + item.fileCount, 0),
- propertyCount: properties.length,
- skillCount: listSkillRegistry().data.summary.skillCount,
- },
- },
- };
- }
- function readKnowledgeFile(nodeId) {
- const { library, filePath, relativePath } = safeNodePath(nodeId);
- if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) throw new Error('知识库文件不存在');
- const extension = path.extname(filePath).toLowerCase();
- if (!ALLOWED_EXTENSIONS.has(extension) || !library.extensions.has(extension)) throw new Error('不支持预览该文件');
- const stats = fs.statSync(filePath);
- if (stats.size > MAX_PREVIEW_BYTES) throw new Error('文件超过 2MB,请缩小后再预览');
- const kind = fileKind(filePath);
- const content = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
- let structured = null;
- if (extension === '.json') {
- try { structured = JSON.parse(content); } catch {}
- }
- return {
- status: 'ok',
- data: {
- id: nodeId,
- name: path.basename(filePath),
- kind,
- library: library.label,
- relativePath: relativePath.replace(/\\/g, '/'),
- size: stats.size,
- modifiedAt: stats.mtime.toISOString(),
- content,
- structured,
- propertyStats: kind === 'property-dataset' ? propertyStats(allProperties()) : null,
- },
- };
- }
- function listProperties(query = {}) {
- const q = String(query.q || '').trim().toLowerCase();
- const district = String(query.district || '').trim();
- const layout = String(query.layout || '').trim();
- const decoration = String(query.decoration || '').trim();
- const maxPrice = Number(query.maxPrice || 0);
- const page = Math.max(1, Number(query.page || 1) || 1);
- const pageSize = Math.max(6, Math.min(60, Number(query.pageSize || 18) || 18));
- const all = allProperties();
- const filtered = all.filter(item => {
- const haystack = `${item.id} ${item.community} ${item.district} ${item.layout} ${(item.highlights || []).join(' ')}`.toLowerCase();
- if (q && !haystack.includes(q)) return false;
- if (district && !item.district.includes(district)) return false;
- if (layout && item.layout !== layout) return false;
- if (decoration && item.decoration !== decoration) return false;
- if (maxPrice && item.totalPrice > maxPrice) return false;
- return true;
- });
- const offset = (page - 1) * pageSize;
- return {
- status: 'ok',
- data: {
- items: filtered.slice(offset, offset + pageSize),
- total: filtered.length,
- page,
- pageSize,
- pageCount: Math.max(1, Math.ceil(filtered.length / pageSize)),
- stats: propertyStats(all),
- source: propertySourcePath(),
- sourceLabel: '演示房源数据集',
- },
- };
- }
- function getProperty(propertyId) {
- const property = allProperties().find(item => item.id === String(propertyId));
- if (!property) throw new Error('房源不存在');
- return { status: 'ok', data: { property, source: propertySourcePath(), sourceLabel: '演示房源数据集' } };
- }
- function parseSkillFrontmatter(content, folderName) {
- const block = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
- const frontmatter = block?.[1] || '';
- const name = frontmatter.match(/^name:\s*(.+)$/m)?.[1]?.trim() || folderName;
- const description = frontmatter.match(/^description:\s*(.+)$/m)?.[1]?.trim() || '';
- const title = content.match(/^#\s+(.+)$/m)?.[1]?.trim() || name;
- return { name, description, title };
- }
- function listSkillRegistry() {
- const packages = packageDefinitions().map(pkg => {
- const skillsRoot = path.join(pkg.root, 'skills');
- const skills = fs.existsSync(skillsRoot)
- ? fs.readdirSync(skillsRoot, { withFileTypes: true }).filter(entry => entry.isDirectory()).map(entry => {
- const skillFile = path.join(skillsRoot, entry.name, 'SKILL.md');
- if (!fs.existsSync(skillFile)) return null;
- const content = fs.readFileSync(skillFile, 'utf8').replace(/^\uFEFF/, '');
- const meta = parseSkillFrontmatter(content, entry.name);
- return {
- id: `${pkg.id}:${entry.name}`,
- packageId: pkg.id,
- folder: entry.name,
- name: meta.name,
- title: meta.title,
- description: meta.description,
- tags: CAPABILITY_TAGS[entry.name] || ['Skill/MCP'],
- page: PAGE_BY_SKILL[entry.name] || '',
- status: pkg.id === 'unified-source' ? 'integrated' : pkg.status,
- content,
- filePath: skillFile,
- };
- }).filter(Boolean)
- : [];
- return {
- id: pkg.id,
- label: pkg.label,
- description: pkg.description,
- status: pkg.status,
- root: pkg.root,
- skillCount: skills.length,
- skills,
- };
- });
- const properties = allProperties();
- packages.push({
- id: 'huaxiang-property-matching',
- label: '花巷房源匹配项目',
- description: '房源数据、客户样本、标签体系与多维匹配引擎。',
- status: 'data-connected',
- root: path.dirname(propertySourcePath()),
- skillCount: 1,
- skills: [{
- id: 'huaxiang-property-matching:property-matching',
- packageId: 'huaxiang-property-matching',
- folder: 'property-matching',
- name: 'property-matching',
- title: '房源智能匹配',
- description: `已接入 ${properties.length} 套演示房源、客户样本、标签和匹配引擎,可作为 Agent 的业务工具与知识数据源。`,
- tags: ['房源列表', '客户画像', '多维评分'],
- page: 'knowledge',
- status: 'integrated',
- content: '# 房源智能匹配\n\n房源数据与匹配引擎已经接入 4320 的知识库页面。\n\n- 数据源:`properties.json`\n- 客户样本:`buyers.json`\n- 标签体系:`buyer-tags.json`\n- 匹配引擎:`match-engine.js`\n',
- filePath: propertySourcePath(),
- }],
- });
- const allSkills = packages.flatMap(item => item.skills);
- const uniqueNames = new Set(allSkills.map(item => item.name));
- return {
- status: 'ok',
- data: {
- packages,
- summary: {
- packageCount: packages.length,
- skillCount: allSkills.length,
- uniqueCapabilityCount: uniqueNames.size,
- integratedCount: allSkills.filter(item => item.status === 'integrated').length,
- },
- },
- };
- }
- function getSkillDetail(skillId) {
- const registry = listSkillRegistry().data;
- const skill = registry.packages.flatMap(item => item.skills.map(entry => ({ ...entry, package: item.label, packageRoot: item.root })))
- .find(item => item.id === skillId);
- if (!skill) throw new Error('技能不存在');
- return { status: 'ok', data: skill };
- }
- module.exports = {
- PROJECT_ROOT,
- PACKAGE_ROOT,
- KNOWLEDGE_BASE_DIR,
- listKnowledgeTree,
- readKnowledgeFile,
- listProperties,
- getProperty,
- listSkillRegistry,
- getSkillDetail,
- };
|