bizgate-mcp-server 0.3.8 → 0.3.10
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/dist/bizgate-cache.d.ts +35 -0
- package/dist/bizgate-cache.js +166 -0
- package/dist/index.js +277 -31
- package/dist/install-skill.js +128 -3
- package/dist/jpx-cache.d.ts +10 -0
- package/dist/jpx-cache.js +39 -19
- package/dist/seed-cache.d.ts +5 -0
- package/dist/seed-cache.js +10 -2
- package/package.json +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface CachedCompany {
|
|
2
|
+
corporate_number: string;
|
|
3
|
+
name: string;
|
|
4
|
+
revenue: string | null;
|
|
5
|
+
industry: string | null;
|
|
6
|
+
emp: string | null;
|
|
7
|
+
prefecture: string | null;
|
|
8
|
+
address: string | null;
|
|
9
|
+
tel: string | null;
|
|
10
|
+
hpurl: string | null;
|
|
11
|
+
pub: string | null;
|
|
12
|
+
shihon: string | null;
|
|
13
|
+
ceo: string | null;
|
|
14
|
+
}
|
|
15
|
+
/** BizGate API の結果を SQLite に永続キャッシュ(ローカル + R2共有) */
|
|
16
|
+
export declare class BizGateResultCache {
|
|
17
|
+
private db;
|
|
18
|
+
private sharedCacheUrl;
|
|
19
|
+
constructor(sharedCacheUrl?: string);
|
|
20
|
+
/** R2から共有キャッシュをダウンロードしてローカルにマージ */
|
|
21
|
+
syncSharedCache(): Promise<void>;
|
|
22
|
+
/** キャッシュから企業情報を取得 */
|
|
23
|
+
get(compno: string): CachedCompany | null;
|
|
24
|
+
/** 企業情報をキャッシュに保存 */
|
|
25
|
+
set(data: CachedCompany): void;
|
|
26
|
+
/** 複数の法人番号を一括チェック → キャッシュにある/ないを分離 */
|
|
27
|
+
partition(compnos: string[]): {
|
|
28
|
+
cached: CachedCompany[];
|
|
29
|
+
uncached: string[];
|
|
30
|
+
};
|
|
31
|
+
/** キャッシュ件数 */
|
|
32
|
+
count(): number;
|
|
33
|
+
/** 新規キャッシュをWorkerにアップロード(バックグラウンド) */
|
|
34
|
+
uploadToWorker(entries: CachedCompany[]): void;
|
|
35
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { mkdirSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
const CACHE_DIR = join(homedir(), ".bizgate", "cache");
|
|
6
|
+
const DB_PATH = join(CACHE_DIR, "bizgate-results.db");
|
|
7
|
+
// SHARED_DB_PATH は JSONL 方式に移行したため不要
|
|
8
|
+
const SHARED_META_PATH = join(CACHE_DIR, "shared-meta.json");
|
|
9
|
+
const SHARED_TTL_DAYS = 1; // 共有キャッシュは毎日チェック
|
|
10
|
+
/** BizGate API の結果を SQLite に永続キャッシュ(ローカル + R2共有) */
|
|
11
|
+
export class BizGateResultCache {
|
|
12
|
+
db;
|
|
13
|
+
sharedCacheUrl;
|
|
14
|
+
constructor(sharedCacheUrl) {
|
|
15
|
+
this.sharedCacheUrl = sharedCacheUrl ?? "";
|
|
16
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
17
|
+
this.db = new Database(DB_PATH);
|
|
18
|
+
this.db.pragma("journal_mode = WAL");
|
|
19
|
+
this.db.exec(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS company_cache (
|
|
21
|
+
corporate_number TEXT PRIMARY KEY,
|
|
22
|
+
name TEXT,
|
|
23
|
+
revenue TEXT,
|
|
24
|
+
industry TEXT,
|
|
25
|
+
emp TEXT,
|
|
26
|
+
prefecture TEXT,
|
|
27
|
+
address TEXT,
|
|
28
|
+
tel TEXT,
|
|
29
|
+
hpurl TEXT,
|
|
30
|
+
pub TEXT,
|
|
31
|
+
shihon TEXT,
|
|
32
|
+
ceo TEXT,
|
|
33
|
+
cached_at TEXT NOT NULL
|
|
34
|
+
);
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
37
|
+
/** R2から共有キャッシュをダウンロードしてローカルにマージ */
|
|
38
|
+
async syncSharedCache() {
|
|
39
|
+
if (!this.sharedCacheUrl)
|
|
40
|
+
return;
|
|
41
|
+
// TTLチェック
|
|
42
|
+
if (existsSync(SHARED_META_PATH)) {
|
|
43
|
+
try {
|
|
44
|
+
const meta = JSON.parse(readFileSync(SHARED_META_PATH, "utf-8"));
|
|
45
|
+
const lastSync = new Date(meta.lastSync);
|
|
46
|
+
const daysDiff = (Date.now() - lastSync.getTime()) / (1000 * 60 * 60 * 24);
|
|
47
|
+
if (daysDiff < SHARED_TTL_DAYS) {
|
|
48
|
+
console.error(`Shared cache is fresh (${Math.round(daysDiff * 24)}h ago), skipping sync`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch { /* proceed with sync */ }
|
|
53
|
+
}
|
|
54
|
+
console.error("Downloading shared BizGate cache from R2...");
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch(this.sharedCacheUrl);
|
|
57
|
+
if (!res.ok || !res.body) {
|
|
58
|
+
console.error(`Shared cache download failed: HTTP ${res.status}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Worker/R2 から JSONL をダウンロードしてローカルにマージ
|
|
62
|
+
const text = await res.text();
|
|
63
|
+
const entries = text
|
|
64
|
+
.split("\n")
|
|
65
|
+
.filter((l) => l.trim())
|
|
66
|
+
.map((l) => {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(l);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
.filter((e) => e !== null);
|
|
75
|
+
if (entries.length > 0) {
|
|
76
|
+
const upsert = this.db.prepare(`
|
|
77
|
+
INSERT OR IGNORE INTO company_cache
|
|
78
|
+
(corporate_number, name, revenue, industry, emp, prefecture, address, tel, hpurl, pub, shihon, ceo, cached_at)
|
|
79
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
80
|
+
`);
|
|
81
|
+
const batchUpsert = this.db.transaction((items) => {
|
|
82
|
+
for (const r of items) {
|
|
83
|
+
upsert.run(r.corporate_number, r.name, r.revenue, r.industry, r.emp, r.prefecture, r.address, r.tel, r.hpurl, r.pub, r.shihon, r.ceo, r.cached_at);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
batchUpsert(entries);
|
|
87
|
+
console.error(`Shared cache merged: ${entries.length} companies`);
|
|
88
|
+
}
|
|
89
|
+
// メタ更新
|
|
90
|
+
writeFileSync(SHARED_META_PATH, JSON.stringify({ lastSync: new Date().toISOString() }), "utf-8");
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
console.error(`Shared cache sync error: ${e instanceof Error ? e.message : e}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** キャッシュから企業情報を取得 */
|
|
97
|
+
get(compno) {
|
|
98
|
+
const row = this.db.prepare("SELECT * FROM company_cache WHERE corporate_number = ?").get(compno);
|
|
99
|
+
if (!row)
|
|
100
|
+
return null;
|
|
101
|
+
return {
|
|
102
|
+
corporate_number: row.corporate_number,
|
|
103
|
+
name: row.name,
|
|
104
|
+
revenue: row.revenue,
|
|
105
|
+
industry: row.industry,
|
|
106
|
+
emp: row.emp,
|
|
107
|
+
prefecture: row.prefecture,
|
|
108
|
+
address: row.address,
|
|
109
|
+
tel: row.tel,
|
|
110
|
+
hpurl: row.hpurl,
|
|
111
|
+
pub: row.pub,
|
|
112
|
+
shihon: row.shihon,
|
|
113
|
+
ceo: row.ceo,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** 企業情報をキャッシュに保存 */
|
|
117
|
+
set(data) {
|
|
118
|
+
this.db.prepare(`
|
|
119
|
+
INSERT OR REPLACE INTO company_cache
|
|
120
|
+
(corporate_number, name, revenue, industry, emp, prefecture, address, tel, hpurl, pub, shihon, ceo, cached_at)
|
|
121
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
122
|
+
`).run(data.corporate_number, data.name, data.revenue, data.industry, data.emp, data.prefecture, data.address, data.tel, data.hpurl, data.pub, data.shihon, data.ceo, new Date().toISOString());
|
|
123
|
+
}
|
|
124
|
+
/** 複数の法人番号を一括チェック → キャッシュにある/ないを分離 */
|
|
125
|
+
partition(compnos) {
|
|
126
|
+
const cached = [];
|
|
127
|
+
const uncached = [];
|
|
128
|
+
for (const compno of compnos) {
|
|
129
|
+
const hit = this.get(compno);
|
|
130
|
+
if (hit) {
|
|
131
|
+
cached.push(hit);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
uncached.push(compno);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { cached, uncached };
|
|
138
|
+
}
|
|
139
|
+
/** キャッシュ件数 */
|
|
140
|
+
count() {
|
|
141
|
+
const row = this.db.prepare("SELECT COUNT(*) as cnt FROM company_cache").get();
|
|
142
|
+
return row.cnt;
|
|
143
|
+
}
|
|
144
|
+
/** 新規キャッシュをWorkerにアップロード(バックグラウンド) */
|
|
145
|
+
uploadToWorker(entries) {
|
|
146
|
+
if (!this.sharedCacheUrl || entries.length === 0)
|
|
147
|
+
return;
|
|
148
|
+
// Worker URL を推測 (download URL → upload URL)
|
|
149
|
+
const workerUrl = process.env.CACHE_WORKER_URL ?? "https://bizgate-cache-worker.a-adachi.workers.dev";
|
|
150
|
+
if (!workerUrl)
|
|
151
|
+
return;
|
|
152
|
+
const payload = entries.map((e) => ({
|
|
153
|
+
...e,
|
|
154
|
+
cached_at: new Date().toISOString(),
|
|
155
|
+
}));
|
|
156
|
+
// 非同期で送信(失敗しても無視)
|
|
157
|
+
fetch(`${workerUrl}/upload`, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: {
|
|
160
|
+
"Content-Type": "application/json",
|
|
161
|
+
"Authorization": `Bearer ${process.env.CACHE_WORKER_TOKEN ?? ""}`,
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify(payload),
|
|
164
|
+
}).catch(() => { });
|
|
165
|
+
}
|
|
166
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { BizGateClient, first as f } from "./bizgate-client.js";
|
|
|
12
12
|
import { BizGateApiError, DEPARTMENT_CATEGORIES } from "./types.js";
|
|
13
13
|
import { UsageTracker } from "./usage-tracker.js";
|
|
14
14
|
import { SeedCache } from "./seed-cache.js";
|
|
15
|
+
import { BizGateResultCache } from "./bizgate-cache.js";
|
|
15
16
|
import { JpxCache } from "./jpx-cache.js";
|
|
16
17
|
import { homedir } from "node:os";
|
|
17
18
|
import { join, dirname } from "node:path";
|
|
@@ -38,8 +39,9 @@ if (authMode === "ip" && !process.env.BIZGATE_APP) {
|
|
|
38
39
|
console.error("Error: BIZGATE_APP is required when BIZGATE_AUTH_MODE=ip");
|
|
39
40
|
process.exit(1);
|
|
40
41
|
}
|
|
41
|
-
const dailyLimit = Number(process.env.BIZGATE_DAILY_LIMIT ?? "
|
|
42
|
+
const dailyLimit = Number(process.env.BIZGATE_DAILY_LIMIT ?? "1000");
|
|
42
43
|
const seedCsvUrl = process.env.SEED_CSV_URL ?? "https://pub-3952dc5d8b37475196bab871e4e93bc1.r2.dev/latest/seed-list.csv.gz";
|
|
44
|
+
const sharedCacheUrl = process.env.SHARED_CACHE_URL ?? "https://bizgate-cache-worker.a-adachi.workers.dev/download";
|
|
43
45
|
const usageFile = process.env.BIZGATE_USAGE_FILE ??
|
|
44
46
|
join(homedir(), ".bizgate-mcp-usage.json");
|
|
45
47
|
const baseUrl = process.env.BIZGATE_BASE_URL ??
|
|
@@ -61,6 +63,7 @@ const config = {
|
|
|
61
63
|
const usageTracker = new UsageTracker(usageFile, dailyLimit);
|
|
62
64
|
const client = new BizGateClient(config, usageTracker);
|
|
63
65
|
const seedCache = new SeedCache(seedCsvUrl);
|
|
66
|
+
const resultCache = new BizGateResultCache(sharedCacheUrl);
|
|
64
67
|
const jpxCache = new JpxCache(process.env.JPX_DATA_URL);
|
|
65
68
|
// ---------- MCPサーバー ----------
|
|
66
69
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -427,20 +430,27 @@ server.tool("bizgate__usage_status", "本日のBizGate API残り利用回数を
|
|
|
427
430
|
});
|
|
428
431
|
// ---------- Tool 7: プロスペクトリスト(ローカルSQLite) ----------
|
|
429
432
|
const MAX_API_CALLS_HARD_LIMIT = 150;
|
|
433
|
+
/** 全角数字を半角に変換 */
|
|
434
|
+
function zenToHan(s) {
|
|
435
|
+
return s.replace(/[0-9]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 0xFEE0));
|
|
436
|
+
}
|
|
430
437
|
/** 売上カテゴリ文字列から数値を抽出して最低ラインと比較 */
|
|
431
438
|
function revenueMatchesMin(revenue, minLabel) {
|
|
432
439
|
if (!revenue)
|
|
433
440
|
return false;
|
|
434
|
-
// revenue例: "
|
|
441
|
+
// revenue例: "5.500億以上" (全角数字+全角ピリオド)
|
|
442
|
+
// カテゴリ: 1=5億未満, 2=5-20億, 3=20-100億, 4=100-500億, 5=500億以上
|
|
435
443
|
const thresholds = {
|
|
436
|
-
"50億以上":
|
|
437
|
-
"300億以上":
|
|
438
|
-
"1000億以上":
|
|
444
|
+
"50億以上": 3, // カテゴリ3(20億-100億) 以上 — 50億含む可能性
|
|
445
|
+
"300億以上": 4, // カテゴリ4(100億-500億) 以上
|
|
446
|
+
"1000億以上": 5, // カテゴリ5(500億以上)
|
|
439
447
|
};
|
|
440
448
|
const minCat = thresholds[minLabel];
|
|
441
449
|
if (!minCat)
|
|
442
|
-
return true;
|
|
443
|
-
|
|
450
|
+
return true;
|
|
451
|
+
// 全角数字・全角ピリオドを半角に正規化してからマッチ
|
|
452
|
+
const normalized = zenToHan(revenue).replace(/./g, ".");
|
|
453
|
+
const match = normalized.match(/^(\d+)\./);
|
|
444
454
|
if (!match)
|
|
445
455
|
return false;
|
|
446
456
|
return Number(match[1]) >= minCat;
|
|
@@ -462,10 +472,10 @@ server.tool("bizgate__prospect_list", "シードリスト(国税庁572万法
|
|
|
462
472
|
city: z.string().optional().describe("市区町村名(例: 千代田区)"),
|
|
463
473
|
industry: z.string().optional().describe("BizGate業種分類キーワード(例: 製造業, サービス, IT)"),
|
|
464
474
|
revenue_min: z.string().optional().describe("最低売上(50億以上 / 300億以上 / 1000億以上)"),
|
|
465
|
-
|
|
475
|
+
emp_min: z.string().optional().describe("最低従業員数(100人以上 / 300人以上 / 1000人以上)"),
|
|
466
476
|
proposal_axis: z.union([z.string(), z.array(z.string())]).optional().describe("提案軸(DOMO / 営業DX/CRM)。部署検索のキーワードに変換される"),
|
|
467
477
|
count: z.number().optional().describe("取得したい企業数(デフォルト: 10、最大: 30)"),
|
|
468
|
-
}, async ({ pref, city, industry, revenue_min,
|
|
478
|
+
}, async ({ pref, city, industry, revenue_min, emp_min, proposal_axis, count: rawCount }) => {
|
|
469
479
|
if (!seedCache.isReady()) {
|
|
470
480
|
return {
|
|
471
481
|
content: [{ type: "text", text: "エラー: シードリストが利用できません。SEED_CSV_URL を設定し、Claude Code を再起動してください。" }],
|
|
@@ -490,18 +500,90 @@ server.tool("bizgate__prospect_list", "シードリスト(国税庁572万法
|
|
|
490
500
|
content: [{ type: "text", text: `条件に合う企業がシードリストに見つかりませんでした。${usageFooter()}` }],
|
|
491
501
|
};
|
|
492
502
|
}
|
|
493
|
-
// ---- Step 2: BizGate企業検索 → 売上/業種/従業員フィルタ ----
|
|
503
|
+
// ---- Step 2: キャッシュ確認 → BizGate企業検索 → 売上/業種/従業員フィルタ ----
|
|
494
504
|
const verified = [];
|
|
505
|
+
const newCacheEntries = [];
|
|
495
506
|
let apiCalls = 0;
|
|
507
|
+
let cacheHits = 0;
|
|
496
508
|
const batchSize = 5;
|
|
497
|
-
const targetVerified = Math.ceil(count * 1.5);
|
|
509
|
+
const targetVerified = Math.ceil(count * 1.5);
|
|
510
|
+
// フィルタ関数(キャッシュ・API結果の両方で使う)
|
|
511
|
+
function matchesFilters(revenue, industryVal, empVal) {
|
|
512
|
+
if (revenue_min && !revenueMatchesMin(revenue ?? undefined, revenue_min))
|
|
513
|
+
return false;
|
|
514
|
+
if (industry) {
|
|
515
|
+
const ind = industryVal ?? "";
|
|
516
|
+
if (!industry.split(",").some((k) => ind.includes(k.trim())))
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
if (emp_min && empVal) {
|
|
520
|
+
const empThresholds = {
|
|
521
|
+
"100人以上": 4, // カテゴリ4(100-300人) 以上
|
|
522
|
+
"300人以上": 5, // カテゴリ5(300-1000人) 以上
|
|
523
|
+
"1000人以上": 6, // カテゴリ6(1000人以上)
|
|
524
|
+
};
|
|
525
|
+
const minEmpCat = empThresholds[emp_min];
|
|
526
|
+
if (minEmpCat) {
|
|
527
|
+
const empNorm = zenToHan(empVal).replace(/./g, ".");
|
|
528
|
+
const empCat = Number(empNorm.match(/^(\d+)\./)?.[1] ?? 0);
|
|
529
|
+
if (empCat < minEmpCat)
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
498
535
|
for (let i = 0; i < seeds.length && verified.length < targetVerified && apiCalls < MAX_API_CALLS_HARD_LIMIT; i += batchSize) {
|
|
499
536
|
const batch = seeds.slice(i, i + batchSize);
|
|
500
537
|
const results = await Promise.all(batch.map(async (seed) => {
|
|
538
|
+
// キャッシュ確認
|
|
539
|
+
const cached = resultCache.get(seed.corporate_number);
|
|
540
|
+
if (cached) {
|
|
541
|
+
cacheHits++;
|
|
542
|
+
if (!matchesFilters(cached.revenue, cached.industry, cached.emp)) {
|
|
543
|
+
return { seed, doc: null };
|
|
544
|
+
}
|
|
545
|
+
// キャッシュヒット → BizGateDocとして組み立て(API消費なし)
|
|
546
|
+
const fakeDoc = {
|
|
547
|
+
compno: cached.corporate_number,
|
|
548
|
+
shogo: [cached.name ?? ""],
|
|
549
|
+
revenue: cached.revenue ?? undefined,
|
|
550
|
+
gyoshu_facet: cached.industry ? [cached.industry] : undefined,
|
|
551
|
+
emp: cached.emp ?? undefined,
|
|
552
|
+
add: cached.address ? [cached.address] : undefined,
|
|
553
|
+
pref: cached.prefecture ? [cached.prefecture] : undefined,
|
|
554
|
+
tel: cached.tel ? [cached.tel] : undefined,
|
|
555
|
+
hpurl: cached.hpurl ? [cached.hpurl] : undefined,
|
|
556
|
+
pub: cached.pub ?? undefined,
|
|
557
|
+
shihon: cached.shihon ?? undefined,
|
|
558
|
+
ceo: cached.ceo ? [cached.ceo] : undefined,
|
|
559
|
+
};
|
|
560
|
+
return { seed, doc: fakeDoc };
|
|
561
|
+
}
|
|
562
|
+
// キャッシュなし → API呼び出し
|
|
501
563
|
try {
|
|
502
564
|
const { docs } = await client.searchCompany({ compno: seed.corporate_number });
|
|
503
565
|
apiCalls++;
|
|
504
|
-
|
|
566
|
+
const doc = docs[0] ?? null;
|
|
567
|
+
// 結果をキャッシュに保存(マッチしなくてもキャッシュ → 次回API不要)
|
|
568
|
+
if (doc) {
|
|
569
|
+
const cached = {
|
|
570
|
+
corporate_number: seed.corporate_number,
|
|
571
|
+
name: f(doc.shogo),
|
|
572
|
+
revenue: doc.revenue ?? null,
|
|
573
|
+
industry: f(doc.gyoshu_facet) || null,
|
|
574
|
+
emp: doc.emp ?? null,
|
|
575
|
+
prefecture: f(doc.pref) || null,
|
|
576
|
+
address: f(doc.add) || null,
|
|
577
|
+
tel: f(doc.tel) || null,
|
|
578
|
+
hpurl: f(doc.hpurl) || null,
|
|
579
|
+
pub: doc.pub ?? null,
|
|
580
|
+
shihon: doc.shihon ?? null,
|
|
581
|
+
ceo: f(doc.ceo) || null,
|
|
582
|
+
};
|
|
583
|
+
resultCache.set(cached);
|
|
584
|
+
newCacheEntries.push(cached);
|
|
585
|
+
}
|
|
586
|
+
return { seed, doc };
|
|
505
587
|
}
|
|
506
588
|
catch {
|
|
507
589
|
apiCalls++;
|
|
@@ -511,22 +593,8 @@ server.tool("bizgate__prospect_list", "シードリスト(国税庁572万法
|
|
|
511
593
|
for (const { seed, doc } of results) {
|
|
512
594
|
if (!doc)
|
|
513
595
|
continue;
|
|
514
|
-
if (
|
|
596
|
+
if (!matchesFilters(doc.revenue, f(doc.gyoshu_facet), doc.emp))
|
|
515
597
|
continue;
|
|
516
|
-
if (industry) {
|
|
517
|
-
const docIndustry = f(doc.gyoshu_facet);
|
|
518
|
-
if (!industry.split(",").some((k) => docIndustry.includes(k.trim())))
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
if (employee_500plus && doc.emp) {
|
|
522
|
-
const empMatch = doc.emp.match(/(\d+)/);
|
|
523
|
-
if (empMatch) {
|
|
524
|
-
// emp例: "6. 500-1000人未満" → カテゴリ6以上
|
|
525
|
-
const empCat = Number(doc.emp.match(/^(\d+)\./)?.[1] ?? 0);
|
|
526
|
-
if (empCat < 6)
|
|
527
|
-
continue; // カテゴリ6 = 500-1000人
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
598
|
verified.push({ doc, seed });
|
|
531
599
|
}
|
|
532
600
|
}
|
|
@@ -534,7 +602,7 @@ server.tool("bizgate__prospect_list", "シードリスト(国税庁572万法
|
|
|
534
602
|
return {
|
|
535
603
|
content: [{
|
|
536
604
|
type: "text",
|
|
537
|
-
text: `条件に合う企業が見つかりませんでした。\n(シード候補: ${seeds.length}社 / API照会: ${apiCalls}
|
|
605
|
+
text: `条件に合う企業が見つかりませんでした。\n(シード候補: ${seeds.length}社 / API照会: ${apiCalls}回 / キャッシュヒット: ${cacheHits}件)${usageFooter()}`,
|
|
538
606
|
}],
|
|
539
607
|
};
|
|
540
608
|
}
|
|
@@ -619,7 +687,11 @@ server.tool("bizgate__prospect_list", "シードリスト(国税庁572万法
|
|
|
619
687
|
return `| ${i + 1} | ${p.name} | ${p.department} | ${keymanStr} | ${p.phone} | ${p.approach_note} |`;
|
|
620
688
|
})
|
|
621
689
|
.join("\n");
|
|
622
|
-
|
|
690
|
+
// 新規キャッシュをWorkerに非同期アップロード
|
|
691
|
+
if (newCacheEntries.length > 0) {
|
|
692
|
+
resultCache.uploadToWorker(newCacheEntries);
|
|
693
|
+
}
|
|
694
|
+
const stats = `\n\n---\nシード: ${seedTotal}社中${seeds.length}社取得 / API照会: ${apiCalls}回 / キャッシュヒット: ${cacheHits}件 / 新規キャッシュ: ${newCacheEntries.length}件 / マッチ: ${prospects.length}社 / キャッシュ総数: ${resultCache.count()}件`;
|
|
623
695
|
return {
|
|
624
696
|
content: [{ type: "text", text: header + table + stats + usageFooter() }],
|
|
625
697
|
};
|
|
@@ -684,7 +756,14 @@ server.tool("bizgate__enterprise_search", "上場企業(JPX約3,800社)か
|
|
|
684
756
|
: { shogo: listed.name };
|
|
685
757
|
const { docs } = await client.searchCompany(searchParams);
|
|
686
758
|
apiCalls++;
|
|
687
|
-
|
|
759
|
+
let doc = docs[0] ?? null;
|
|
760
|
+
// shogo検索ではrevenueが返らないため、compnoで再検索
|
|
761
|
+
if (doc && !doc.revenue && !listed.corporate_number && doc.compno) {
|
|
762
|
+
const { docs: docs2 } = await client.searchCompany({ compno: doc.compno });
|
|
763
|
+
apiCalls++;
|
|
764
|
+
doc = docs2[0] ?? doc;
|
|
765
|
+
}
|
|
766
|
+
return { listed, doc };
|
|
688
767
|
}
|
|
689
768
|
catch {
|
|
690
769
|
apiCalls++;
|
|
@@ -777,10 +856,177 @@ server.tool("bizgate__enterprise_search", "上場企業(JPX約3,800社)か
|
|
|
777
856
|
content: [{ type: "text", text: header + filterInfo + table + stats + usageFooter() }],
|
|
778
857
|
};
|
|
779
858
|
});
|
|
859
|
+
// ---------- Tool 9: prospect_suggest(ローカルDB候補+Claude判断 → API節約) ----------
|
|
860
|
+
server.tool("bizgate__prospect_suggest", "BizGate APIを使わずにローカルDB(上場企業JPX約3,800社 or シード約570万社)から候補企業リストを返す。sourceを省略すると利用可能な検索条件のガイドを返す(条件を知らないユーザー向け)。Claudeがリストから提案軸に合う企業を判断し、選んだ企業だけをbizgate__department_searchで照会する2段階ワークフロー用。API消費: 0回。", {
|
|
861
|
+
source: z.enum(["jpx", "seed"]).optional().describe("検索ソース。jpx=上場企業(市場・業種フィルタ可)、seed=全企業(都道府県・市区町村フィルタ可)。省略時は検索条件ガイドを表示"),
|
|
862
|
+
pref: z.string().optional().describe("都道府県(例: 東京都)。sourceがseedの場合のみ有効"),
|
|
863
|
+
city: z.string().optional().describe("市区町村(例: 渋谷区)。sourceがseedの場合のみ有効"),
|
|
864
|
+
keyword: z.string().optional().describe("企業名キーワード(部分一致)"),
|
|
865
|
+
market: z.string().optional().describe("市場区分(プライム / スタンダード / グロース)。sourceがjpxの場合のみ有効"),
|
|
866
|
+
industry: z.string().optional().describe("業種キーワード(例: 情報・通信業, 電気機器)。sourceがjpxの場合のみ有効"),
|
|
867
|
+
count: z.number().optional().describe("取得件数(デフォルト: 50、最大: 200)"),
|
|
868
|
+
shuffle: z.boolean().optional().describe("ランダム順にするか(デフォルト: false)"),
|
|
869
|
+
}, async ({ source, pref, city, keyword, market, industry, count: rawCount, shuffle }) => {
|
|
870
|
+
// ---- ガイドモード: sourceが省略された場合 ----
|
|
871
|
+
if (!source) {
|
|
872
|
+
const lines = [
|
|
873
|
+
"## 🔍 プロスペクト検索 — 条件ガイド",
|
|
874
|
+
"",
|
|
875
|
+
"このツールはローカルDBから候補企業を抽出します(**API消費 0回**)。",
|
|
876
|
+
"以下の条件を組み合わせて検索できます。",
|
|
877
|
+
"",
|
|
878
|
+
"---",
|
|
879
|
+
"",
|
|
880
|
+
"### データソース(`source` — 必須)",
|
|
881
|
+
"",
|
|
882
|
+
"| source | 対象 | 件数 | 使えるフィルタ |",
|
|
883
|
+
"|--------|------|------|--------------|",
|
|
884
|
+
"| `jpx` | 上場企業 | 約3,800社 | 市場区分, 業種, 企業名 |",
|
|
885
|
+
"| `seed` | 全法人(国税庁) | 約572万社 | 都道府県, 市区町村, 企業名 |",
|
|
886
|
+
"",
|
|
887
|
+
];
|
|
888
|
+
// JPX: 市場別
|
|
889
|
+
if (jpxCache.isReady()) {
|
|
890
|
+
const markets = jpxCache.listMarkets();
|
|
891
|
+
lines.push("### 市場区分(`market` — jpxのみ)");
|
|
892
|
+
lines.push("");
|
|
893
|
+
lines.push("| 市場 | 企業数 |");
|
|
894
|
+
lines.push("|------|--------|");
|
|
895
|
+
for (const m of markets) {
|
|
896
|
+
lines.push(`| ${m.market} | ${m.count.toLocaleString()}社 |`);
|
|
897
|
+
}
|
|
898
|
+
// JPX: 業種
|
|
899
|
+
const industries = jpxCache.listIndustries();
|
|
900
|
+
lines.push("");
|
|
901
|
+
lines.push("### 業種(`industry` — jpxのみ、部分一致)");
|
|
902
|
+
lines.push("");
|
|
903
|
+
lines.push("| 業種 | 企業数 |");
|
|
904
|
+
lines.push("|------|--------|");
|
|
905
|
+
for (const ind of industries) {
|
|
906
|
+
lines.push(`| ${ind.industry} | ${ind.count.toLocaleString()}社 |`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
// Seed: 都道府県
|
|
910
|
+
if (seedCache.isReady()) {
|
|
911
|
+
const prefs = seedCache.listPrefectures();
|
|
912
|
+
lines.push("");
|
|
913
|
+
lines.push("### 都道府県(`pref` — seedのみ、完全一致)");
|
|
914
|
+
lines.push("");
|
|
915
|
+
lines.push("| 都道府県 | 法人数 |");
|
|
916
|
+
lines.push("|---------|--------|");
|
|
917
|
+
for (const p of prefs) {
|
|
918
|
+
lines.push(`| ${p.prefecture} | ${p.count.toLocaleString()}社 |`);
|
|
919
|
+
}
|
|
920
|
+
lines.push("");
|
|
921
|
+
lines.push("### 市区町村(`city` — seedのみ、完全一致)");
|
|
922
|
+
lines.push("都道府県と組み合わせて使用。例: `pref: \"東京都\", city: \"渋谷区\"`");
|
|
923
|
+
}
|
|
924
|
+
// 共通オプション
|
|
925
|
+
lines.push("");
|
|
926
|
+
lines.push("### 共通オプション");
|
|
927
|
+
lines.push("");
|
|
928
|
+
lines.push("| パラメータ | 説明 |");
|
|
929
|
+
lines.push("|-----------|------|");
|
|
930
|
+
lines.push("| `keyword` | 企業名キーワード(部分一致) |");
|
|
931
|
+
lines.push("| `count` | 取得件数(デフォルト50、最大200) |");
|
|
932
|
+
lines.push("| `shuffle` | ランダム順にするか(デフォルトfalse) |");
|
|
933
|
+
// BizGate API フィルタ(prospect_list / enterprise_search で使用)
|
|
934
|
+
lines.push("");
|
|
935
|
+
lines.push("---");
|
|
936
|
+
lines.push("");
|
|
937
|
+
lines.push("### 参考: BizGate APIフィルタ(`prospect_list` / `enterprise_search` で使用)");
|
|
938
|
+
lines.push("");
|
|
939
|
+
lines.push("以下はAPI消費ありのツールで使える追加フィルタです。");
|
|
940
|
+
lines.push("");
|
|
941
|
+
lines.push("**売上規模(`revenue_min`)**");
|
|
942
|
+
lines.push("| ラベル | BizGateカテゴリ |");
|
|
943
|
+
lines.push("|--------|---------------|");
|
|
944
|
+
lines.push("| `50億以上` | カテゴリ3~(20億-100億以上) |");
|
|
945
|
+
lines.push("| `300億以上` | カテゴリ4~(100億-500億以上) |");
|
|
946
|
+
lines.push("| `1000億以上` | カテゴリ5(500億以上) |");
|
|
947
|
+
lines.push("");
|
|
948
|
+
lines.push("**従業員数(`emp_min`)**");
|
|
949
|
+
lines.push("| ラベル | BizGateカテゴリ |");
|
|
950
|
+
lines.push("|--------|---------------|");
|
|
951
|
+
lines.push("| `100人以上` | カテゴリ4~(100-300人以上) |");
|
|
952
|
+
lines.push("| `300人以上` | カテゴリ5~(300-1000人以上) |");
|
|
953
|
+
lines.push("| `1000人以上` | カテゴリ6(1000人以上) |");
|
|
954
|
+
lines.push("");
|
|
955
|
+
lines.push("**提案軸(`proposal_axis`)**");
|
|
956
|
+
lines.push("| 軸 | 自動変換される部署キーワード |");
|
|
957
|
+
lines.push("|-----|---------------------------|");
|
|
958
|
+
lines.push("| `DOMO` | 経営企画, DX, システム, 情報, データ, 分析 |");
|
|
959
|
+
lines.push("| `営業DX/CRM` | 営業, マーケティング, 販売, 顧客, CRM, 販促 |");
|
|
960
|
+
return {
|
|
961
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
// ---- JPXソース ----
|
|
965
|
+
const count = Math.min(rawCount ?? 50, 200);
|
|
966
|
+
if (source === "jpx") {
|
|
967
|
+
if (!jpxCache.isReady()) {
|
|
968
|
+
return {
|
|
969
|
+
content: [{ type: "text", text: "エラー: 上場企業データベースが利用できません。サーバーを再起動してください。" }],
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
const { total, results } = jpxCache.search({
|
|
973
|
+
market,
|
|
974
|
+
industry,
|
|
975
|
+
nameKeyword: keyword,
|
|
976
|
+
limit: count,
|
|
977
|
+
});
|
|
978
|
+
if (results.length === 0) {
|
|
979
|
+
return {
|
|
980
|
+
content: [{ type: "text", text: `条件に合う上場企業が見つかりませんでした。(検索条件: 市場=${market ?? "全て"}, 業種=${industry ?? "全て"}, キーワード=${keyword ?? "なし"})` }],
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
const table = "| # | 証券コード | 会社名 | 市場 | 業種 | 法人番号 |\n" +
|
|
984
|
+
"|---|-----------|--------|------|------|----------|\n" +
|
|
985
|
+
results
|
|
986
|
+
.map((r, i) => `| ${i + 1} | ${r.securities_code} | ${r.name} | ${r.market} | ${r.industry_33} | ${r.corporate_number ?? "-"} |`)
|
|
987
|
+
.join("\n");
|
|
988
|
+
return {
|
|
989
|
+
content: [{
|
|
990
|
+
type: "text",
|
|
991
|
+
text: `## 上場企業候補(${results.length}社 / 全${total}社)\n\n${table}\n\n---\nAPI消費: 0回\n\n> この中から提案軸に合う企業をClaudeが選び、選んだ企業のみ bizgate__department_search で部署・電話番号を取得してください。法人番号(compno)を使うと精度が高くなります。`,
|
|
992
|
+
}],
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
// ---- シードソース ----
|
|
996
|
+
if (!seedCache.isReady()) {
|
|
997
|
+
return {
|
|
998
|
+
content: [{ type: "text", text: "エラー: シードデータベースが利用できません。サーバーを再起動してください。" }],
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
const { total, results } = seedCache.search({
|
|
1002
|
+
pref,
|
|
1003
|
+
city,
|
|
1004
|
+
keyword,
|
|
1005
|
+
limit: count,
|
|
1006
|
+
shuffle: shuffle ?? false,
|
|
1007
|
+
});
|
|
1008
|
+
if (results.length === 0) {
|
|
1009
|
+
return {
|
|
1010
|
+
content: [{ type: "text", text: `条件に合う企業が見つかりませんでした。(検索条件: 都道府県=${pref ?? "全て"}, 市区町村=${city ?? "全て"}, キーワード=${keyword ?? "なし"})` }],
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
const table = "| # | 会社名 | 都道府県 | 市区町村 | 法人番号 |\n" +
|
|
1014
|
+
"|---|--------|---------|---------|----------|\n" +
|
|
1015
|
+
results
|
|
1016
|
+
.map((r, i) => `| ${i + 1} | ${r.name} | ${r.prefecture} | ${r.city} | ${r.corporate_number} |`)
|
|
1017
|
+
.join("\n");
|
|
1018
|
+
return {
|
|
1019
|
+
content: [{
|
|
1020
|
+
type: "text",
|
|
1021
|
+
text: `## シード企業候補(${results.length}社 / 全${total}社)\n\n${table}\n\n---\nAPI消費: 0回\n\n> この中から提案軸に合う企業をClaudeが選び、選んだ企業のみ bizgate__department_search で部署・電話番号を取得してください。法人番号(compno)を使うと精度が高くなります。`,
|
|
1022
|
+
}],
|
|
1023
|
+
};
|
|
1024
|
+
});
|
|
780
1025
|
// ---------- 起動 ----------
|
|
781
1026
|
async function main() {
|
|
782
|
-
//
|
|
1027
|
+
// シードキャッシュ + 共有BizGateキャッシュを非同期で初期化
|
|
783
1028
|
seedCache.init().catch((e) => console.error("Seed cache init error:", e));
|
|
1029
|
+
resultCache.syncSharedCache().catch((e) => console.error("Shared cache sync error:", e));
|
|
784
1030
|
jpxCache.init().catch((e) => console.error("JPX cache init error:", e));
|
|
785
1031
|
const transport = new StdioServerTransport();
|
|
786
1032
|
await server.connect(transport);
|
package/dist/install-skill.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import { mkdirSync, writeFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
const
|
|
5
|
+
const SKILLS_BASE = join(homedir(), ".claude", "skills");
|
|
6
|
+
const SKILL_DIR = join(SKILLS_BASE, "prospect-match");
|
|
7
|
+
const SKILL_FIND_DIR = join(SKILLS_BASE, "prospect-find");
|
|
6
8
|
const SKILL_MD = `---
|
|
7
9
|
name: prospect-match
|
|
8
10
|
description: |
|
|
@@ -90,9 +92,120 @@ bKwd: "戦略,成長,事業創造,イノベーション,企画,開発,推進,DX,
|
|
|
90
92
|
|
|
91
93
|
---
|
|
92
94
|
|
|
95
|
+
$ARGUMENTS
|
|
96
|
+
`;
|
|
97
|
+
const SKILL_FIND_MD = `---
|
|
98
|
+
name: prospect-find
|
|
99
|
+
description: |
|
|
100
|
+
条件を対話的に選択し、ローカルDBから候補企業を抽出 → Claude判断 → 部署検索の低API消費ワークフローを実行する。
|
|
101
|
+
"企業を探して", "プロスペクトを探して", "prospect find", "リスト作って",
|
|
102
|
+
"DOMOに合う企業", "営業先を探して", "ターゲット企業", "候補企業",
|
|
103
|
+
"기업을 찾아줘", "리스트 만들어줘", "영업 대상 찾아줘"
|
|
104
|
+
user-invocable: true
|
|
105
|
+
argument-hint: "[提案サービス名 or 自由条件]"
|
|
106
|
+
allowed-tools: mcp__bizgate__bizgate__prospect_suggest, mcp__bizgate__bizgate__department_search, mcp__bizgate__bizgate__company_search, AskUserQuestion, WebSearch
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
# Prospect Find: 条件ガイド付きプロスペクト探索
|
|
110
|
+
|
|
111
|
+
条件を対話的に確認し、ローカルDBから候補を抽出 → Claude判断 → BizGate部署検索、の3段階で営業ターゲットを発見するスキル。API消費を最小限に抑える。
|
|
112
|
+
|
|
113
|
+
## プロセス
|
|
114
|
+
|
|
115
|
+
### Step 1: 提案軸を確認
|
|
116
|
+
|
|
117
|
+
ユーザーの入力から **何を売りたいか(提案サービス/製品)** を特定する。
|
|
118
|
+
明示されていない場合は \`AskUserQuestion\` で確認する。
|
|
119
|
+
|
|
120
|
+
\\\`\\\`\\\`
|
|
121
|
+
何を提案したいですか?
|
|
122
|
+
|
|
123
|
+
1. DOMO(BI/データ分析ダッシュボード)
|
|
124
|
+
2. 営業代行(営業KPI管理・架電管理)
|
|
125
|
+
3. Solution(内部管理システム・DB構築)
|
|
126
|
+
4. AI(Claude × BizGate MCP連携)
|
|
127
|
+
5. その他(自由入力)
|
|
128
|
+
|
|
129
|
+
番号で選んでください。
|
|
130
|
+
\\\`\\\`\\\`
|
|
131
|
+
|
|
132
|
+
**提案軸 → 部署検索キーワード変換表:**
|
|
133
|
+
|
|
134
|
+
| 提案軸 | bKwd |
|
|
135
|
+
|--------|------|
|
|
136
|
+
| DOMO | 経営企画,DX,システム,情報,データ,分析,デジタル |
|
|
137
|
+
| 営業代行 | 営業,販売,販促,マーケティング,事業開発,顧客 |
|
|
138
|
+
| Solution | ICT,システム,DX,情報,企画,総務,イノベーション,デジタル |
|
|
139
|
+
| AI | 戦略,成長,事業創造,イノベーション,企画,開発,推進,DX,AI,ICT |
|
|
140
|
+
|
|
141
|
+
### Step 2: 検索条件を選択
|
|
142
|
+
|
|
143
|
+
\`AskUserQuestion\` で以下の条件を**1つのメッセージで**提示する。
|
|
144
|
+
不要な条件はスキップ可能であることを明記する。
|
|
145
|
+
|
|
146
|
+
\\\`\\\`\\\`
|
|
147
|
+
検索条件を選んでください(不要なものはスキップOK)。
|
|
148
|
+
|
|
149
|
+
■ データソース
|
|
150
|
+
a. 上場企業のみ(約3,800社 — 市場・業種で絞り込み可)
|
|
151
|
+
b. 全法人(約572万社 — 地域で絞り込み可)
|
|
152
|
+
|
|
153
|
+
■ 地域(全法人の場合)
|
|
154
|
+
→ 都道府県を入力(例: 東京都) — スキップ可
|
|
155
|
+
|
|
156
|
+
■ 市場区分(上場企業の場合)
|
|
157
|
+
1. 全市場 2. プライム 3. スタンダード 4. グロース
|
|
158
|
+
|
|
159
|
+
■ 業種(上場企業の場合)
|
|
160
|
+
→ キーワード入力(例: 情報・通信業) — スキップ可
|
|
161
|
+
|
|
162
|
+
■ 取得件数
|
|
163
|
+
→ デフォルト50社(最大200)
|
|
164
|
+
|
|
165
|
+
例: 「a, プライム, 情報, 100社」のように回答してください。
|
|
166
|
+
\\\`\\\`\\\`
|
|
167
|
+
|
|
168
|
+
### Step 3: prospect_suggest 実行(API 0回)
|
|
169
|
+
|
|
170
|
+
Step 2 の回答をパースして \`bizgate__prospect_suggest\` を呼ぶ。
|
|
171
|
+
|
|
172
|
+
- データソース a → \`source: "jpx"\` + market/industry
|
|
173
|
+
- データソース b → \`source: "seed"\` + pref/city
|
|
174
|
+
|
|
175
|
+
### Step 4: Claude が候補を評価(API 0回)
|
|
176
|
+
|
|
177
|
+
返ってきた候補リストを見て、**提案軸に合う企業を5〜10社選ぶ**。
|
|
178
|
+
選定基準:
|
|
179
|
+
- Claudeの知識ベースで企業の規模・業種・事業内容を判断
|
|
180
|
+
- 提案軸のサービスを導入しそうな企業を優先
|
|
181
|
+
- 選定理由を簡潔に付記する
|
|
182
|
+
|
|
183
|
+
### Step 5: department_search 実行(API N回)
|
|
184
|
+
|
|
185
|
+
選定した企業ごとに \`bizgate__department_search\` を実行する。
|
|
186
|
+
- \`compno\`: 法人番号(候補リストから取得)
|
|
187
|
+
- \`bKwd\`: Step 1 で決めた提案軸のキーワード
|
|
188
|
+
- \`bKOpr\`: "0"(OR検索)
|
|
189
|
+
|
|
190
|
+
**並列実行**で効率化する(最大5社同時)。
|
|
191
|
+
|
|
192
|
+
### Step 6: 結果出力
|
|
193
|
+
|
|
194
|
+
企業ごとにターゲット部署・電話番号・マッチ理由を表形式で出力する。
|
|
195
|
+
|
|
196
|
+
## 注意事項
|
|
197
|
+
|
|
198
|
+
- 電話番号がない場合は「-」と表示
|
|
199
|
+
- 各企業につき最大5部署まで提案
|
|
200
|
+
- マッチ理由は1行で簡潔に
|
|
201
|
+
- ユーザーの言語(日本語 or 韓国語)に合わせて出力
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
93
205
|
$ARGUMENTS
|
|
94
206
|
`;
|
|
95
207
|
export function main() {
|
|
208
|
+
// prospect-match
|
|
96
209
|
const existed = existsSync(join(SKILL_DIR, "SKILL.md"));
|
|
97
210
|
mkdirSync(SKILL_DIR, { recursive: true });
|
|
98
211
|
writeFileSync(join(SKILL_DIR, "SKILL.md"), SKILL_MD, "utf-8");
|
|
@@ -103,10 +216,22 @@ export function main() {
|
|
|
103
216
|
console.log("✔ prospect-match スキルをインストールしました");
|
|
104
217
|
}
|
|
105
218
|
console.log(` 場所: ${SKILL_DIR}/SKILL.md`);
|
|
219
|
+
// prospect-find
|
|
220
|
+
const existed2 = existsSync(join(SKILL_FIND_DIR, "SKILL.md"));
|
|
221
|
+
mkdirSync(SKILL_FIND_DIR, { recursive: true });
|
|
222
|
+
writeFileSync(join(SKILL_FIND_DIR, "SKILL.md"), SKILL_FIND_MD, "utf-8");
|
|
223
|
+
if (existed2) {
|
|
224
|
+
console.log("✔ prospect-find スキルを更新しました");
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
console.log("✔ prospect-find スキルをインストールしました");
|
|
228
|
+
}
|
|
229
|
+
console.log(` 場所: ${SKILL_FIND_DIR}/SKILL.md`);
|
|
106
230
|
console.log("");
|
|
107
231
|
console.log("使い方:");
|
|
108
|
-
console.log(" /prospect-match 会社名");
|
|
109
|
-
console.log(
|
|
232
|
+
console.log(" /prospect-match 会社名 — 特定企業の部署マッチング");
|
|
233
|
+
console.log(" /prospect-find DOMO — 条件ガイド付きプロスペクト探索");
|
|
234
|
+
console.log(' または「DOMOに合う企業を探して」「営業先を探して」');
|
|
110
235
|
}
|
|
111
236
|
// 直接実行された場合のみ実行
|
|
112
237
|
const isDirectRun = process.argv[1]?.endsWith("install-skill.js");
|
package/dist/jpx-cache.d.ts
CHANGED
|
@@ -12,6 +12,16 @@ export declare class JpxCache {
|
|
|
12
12
|
constructor(jpxUrl?: string);
|
|
13
13
|
init(): Promise<void>;
|
|
14
14
|
isReady(): boolean;
|
|
15
|
+
/** 利用可能な業種一覧と件数を返す */
|
|
16
|
+
listIndustries(): {
|
|
17
|
+
industry: string;
|
|
18
|
+
count: number;
|
|
19
|
+
}[];
|
|
20
|
+
/** 市場別の企業数を返す */
|
|
21
|
+
listMarkets(): {
|
|
22
|
+
market: string;
|
|
23
|
+
count: number;
|
|
24
|
+
}[];
|
|
15
25
|
search(params: {
|
|
16
26
|
market?: string;
|
|
17
27
|
industry?: string;
|
package/dist/jpx-cache.js
CHANGED
|
@@ -58,6 +58,22 @@ export class JpxCache {
|
|
|
58
58
|
isReady() {
|
|
59
59
|
return this.ready && this.db !== null;
|
|
60
60
|
}
|
|
61
|
+
/** 利用可能な業種一覧と件数を返す */
|
|
62
|
+
listIndustries() {
|
|
63
|
+
if (!this.db)
|
|
64
|
+
return [];
|
|
65
|
+
return this.db
|
|
66
|
+
.prepare("SELECT industry_33 as industry, COUNT(*) as count FROM listed_companies GROUP BY industry_33 ORDER BY count DESC")
|
|
67
|
+
.all();
|
|
68
|
+
}
|
|
69
|
+
/** 市場別の企業数を返す */
|
|
70
|
+
listMarkets() {
|
|
71
|
+
if (!this.db)
|
|
72
|
+
return [];
|
|
73
|
+
return this.db
|
|
74
|
+
.prepare("SELECT market, COUNT(*) as count FROM listed_companies GROUP BY market ORDER BY count DESC")
|
|
75
|
+
.all();
|
|
76
|
+
}
|
|
61
77
|
search(params) {
|
|
62
78
|
if (!this.db)
|
|
63
79
|
return { total: 0, results: [] };
|
|
@@ -122,7 +138,10 @@ export class JpxCache {
|
|
|
122
138
|
const codeCol = colKeys.find((k) => k.includes("コード")) ?? colKeys[0];
|
|
123
139
|
const nameCol = colKeys.find((k) => k.includes("銘柄名")) ?? colKeys[1];
|
|
124
140
|
const marketCol = colKeys.find((k) => k.includes("市場・商品区分")) ?? colKeys[2];
|
|
125
|
-
|
|
141
|
+
// 33業種区分(ラベル)を優先、なければ33業種コードを使用
|
|
142
|
+
const industry33LabelCol = colKeys.find((k) => k.includes("33業種区分"));
|
|
143
|
+
const industry33CodeCol = colKeys.find((k) => k.includes("33業種コード")) ?? colKeys[4];
|
|
144
|
+
const industry33Col = industry33LabelCol ?? industry33CodeCol;
|
|
126
145
|
console.error(`Column mapping: code=${codeCol}, name=${nameCol}, market=${marketCol}, industry=${industry33Col}`);
|
|
127
146
|
// seed.db を読み書きモードで開く
|
|
128
147
|
const db = new Database(DB_PATH);
|
|
@@ -175,24 +194,25 @@ export class JpxCache {
|
|
|
175
194
|
.get();
|
|
176
195
|
if (hasSeedTable) {
|
|
177
196
|
console.error("Matching with seed_companies...");
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
197
|
+
// 高速マッチング: 「株式会社+銘柄名」「銘柄名+株式会社」「銘柄名そのまま」で完全一致
|
|
198
|
+
const updateSql = `
|
|
199
|
+
UPDATE listed_companies SET corporate_number = (
|
|
200
|
+
SELECT s.corporate_number FROM seed_companies s
|
|
201
|
+
WHERE s.name = '株式会社' || listed_companies.name
|
|
202
|
+
OR s.name = listed_companies.name || '株式会社'
|
|
203
|
+
OR s.name = listed_companies.name
|
|
204
|
+
LIMIT 1
|
|
205
|
+
)
|
|
206
|
+
WHERE corporate_number IS NULL
|
|
207
|
+
`;
|
|
208
|
+
db.prepare(updateSql).run();
|
|
209
|
+
const matched = db
|
|
210
|
+
.prepare("SELECT COUNT(*) as cnt FROM listed_companies WHERE corporate_number IS NOT NULL")
|
|
211
|
+
.get().cnt;
|
|
212
|
+
const total = db
|
|
213
|
+
.prepare("SELECT COUNT(*) as cnt FROM listed_companies")
|
|
214
|
+
.get().cnt;
|
|
215
|
+
console.error(`Matched ${matched}/${total} companies with seed data`);
|
|
196
216
|
}
|
|
197
217
|
// インデックス作成
|
|
198
218
|
db.exec(`
|
package/dist/seed-cache.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export declare class SeedCache {
|
|
|
12
12
|
/** Initialize: check cache, download if needed, open DB */
|
|
13
13
|
init(): Promise<void>;
|
|
14
14
|
isReady(): boolean;
|
|
15
|
+
/** 利用可能な都道府県一覧と件数を返す */
|
|
16
|
+
listPrefectures(): {
|
|
17
|
+
prefecture: string;
|
|
18
|
+
count: number;
|
|
19
|
+
}[];
|
|
15
20
|
/** Search seed companies */
|
|
16
21
|
search(params: {
|
|
17
22
|
pref?: string;
|
package/dist/seed-cache.js
CHANGED
|
@@ -43,6 +43,14 @@ export class SeedCache {
|
|
|
43
43
|
isReady() {
|
|
44
44
|
return this.ready && this.db !== null;
|
|
45
45
|
}
|
|
46
|
+
/** 利用可能な都道府県一覧と件数を返す */
|
|
47
|
+
listPrefectures() {
|
|
48
|
+
if (!this.db)
|
|
49
|
+
return [];
|
|
50
|
+
return this.db
|
|
51
|
+
.prepare("SELECT prefecture, COUNT(*) as count FROM seed_companies WHERE prefecture IS NOT NULL AND prefecture != '' GROUP BY prefecture ORDER BY count DESC")
|
|
52
|
+
.all();
|
|
53
|
+
}
|
|
46
54
|
/** Search seed companies */
|
|
47
55
|
search(params) {
|
|
48
56
|
if (!this.db)
|
|
@@ -122,8 +130,8 @@ export class SeedCache {
|
|
|
122
130
|
const lines = csv.split("\n");
|
|
123
131
|
const batchInsert = db.transaction((rows) => {
|
|
124
132
|
for (const row of rows) {
|
|
125
|
-
if (row.length >=
|
|
126
|
-
insert.run(row[0], row[1], row[
|
|
133
|
+
if (row.length >= 6) {
|
|
134
|
+
insert.run(row[0], row[1], row[4], row[5]);
|
|
127
135
|
}
|
|
128
136
|
}
|
|
129
137
|
});
|