@horizon_works/banto 0.5.0 → 0.6.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/gate_core.js +7 -1
- package/license/client.js +77 -1
- package/package.json +45 -45
- package/privacy.js +74 -9
- package/server.js +51 -3
package/gate_core.js
CHANGED
|
@@ -199,8 +199,14 @@ function summarize(r, dict, targetLabel) {
|
|
|
199
199
|
dictionary_free_candidates: r.candidates.length,
|
|
200
200
|
verdict: reasons.length ? '要確認' : 'この検査では検出なし',
|
|
201
201
|
verdict_reasons: reasons,
|
|
202
|
+
// 🚨 ★フルパスを返さない(2026-08-21 実測で判明)。
|
|
203
|
+
// Windows の一時フォルダは C:\Users\<ユーザー名>\... なので、
|
|
204
|
+
// **利用者本人の名前がAIへ渡っていた**。本文は1文字も渡していないのに、
|
|
205
|
+
// 「件数と判定だけ」という言い方が正確でなくなる。
|
|
206
|
+
// ★便利にしようとした1行が、設計を崩す(exposure-boundary §4)。
|
|
202
207
|
detail_report: reportPath
|
|
203
|
-
? '
|
|
208
|
+
? '詳細は、お使いのパソコンの一時フォルダ(Windowsなら %TEMP%)の '
|
|
209
|
+
+ path.basename(reportPath) + ' に書きました。★人が手元で開いてください(AIは読みません)'
|
|
204
210
|
: null,
|
|
205
211
|
note:
|
|
206
212
|
'★AIへ返しているのは件数と判定だけです。本文・該当行・該当語は返していません。' +
|
package/license/client.js
CHANGED
|
@@ -135,4 +135,80 @@ function decide(c, now) {
|
|
|
135
135
|
message: 'この鍵が確認できませんでした。お手数ですがご連絡ください' };
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
// ═══════════════════════════════════════════════════════════════
|
|
139
|
+
// 型キーの統計(段1)★既定オフのオプトイン
|
|
140
|
+
//
|
|
141
|
+
// ★送るのは「型キーの名前」と「件数」だけ。記録の本文・顧客名・文章は一切送らない。
|
|
142
|
+
// → 送る中身は buildStats() が作る。**ここ以外で組み立てない**(口を1つにする)。
|
|
143
|
+
// ★1日1回だけ。相手が「はい」と言うまで、この関数は一度も呼ばれない。
|
|
144
|
+
// ═══════════════════════════════════════════════════════════════
|
|
145
|
+
|
|
146
|
+
// ★お手元のフォルダに置く。★パッケージの中に置くと、版が上がったとき npx が入れ替えて消える
|
|
147
|
+
// (=同じ日に2回送ってしまう。DB側は潰すので実害は無いが、印は残るところに置く)
|
|
148
|
+
const HOME = process.env.BANTO_HOME || path.join(require('os').homedir(), '.banto');
|
|
149
|
+
const STATS_CACHE = path.join(HOME, '.stats.json');
|
|
150
|
+
|
|
151
|
+
/** 送ることに同意しているか(★既定オフ。ファイルが無ければオフ) */
|
|
152
|
+
function isSharing() {
|
|
153
|
+
try { return JSON.parse(fs.readFileSync(path.join(HOME, 'share.json'), 'utf8')).stats === true; }
|
|
154
|
+
catch (_) { return false; }
|
|
155
|
+
}
|
|
156
|
+
/** 同意を切り替える(★人が明示的に呼んだときだけ) */
|
|
157
|
+
function setSharing(on) {
|
|
158
|
+
try { fs.mkdirSync(HOME, { recursive: true }); } catch (_) {}
|
|
159
|
+
const f = path.join(HOME, 'share.json');
|
|
160
|
+
const rec = { stats: on === true, changed_at: new Date().toISOString() };
|
|
161
|
+
fs.writeFileSync(f, JSON.stringify(rec, null, 2));
|
|
162
|
+
return rec;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* review() の結果から、送る形だけを取り出す
|
|
167
|
+
* ★型キーの名前と数以外は、ここで落ちる(本文が通る経路を作らない)
|
|
168
|
+
*/
|
|
169
|
+
function buildStats(review) {
|
|
170
|
+
if (!review) return [];
|
|
171
|
+
const out = [];
|
|
172
|
+
const push = (sig, total, afterRuled) => {
|
|
173
|
+
if (!sig) return;
|
|
174
|
+
out.push({ sig: String(sig).slice(0, 64), total: Number(total) || 0,
|
|
175
|
+
after_ruled: afterRuled === undefined || afterRuled === null ? null : Number(afterRuled) });
|
|
176
|
+
};
|
|
177
|
+
for (const r of review['ルールにする番'] || review.to_rule || []) push(r.sig, r.count, null);
|
|
178
|
+
for (const r of review['ルールが効いていない'] || review.not_working || []) push(r.sig, r.since_ruled, r.since_ruled);
|
|
179
|
+
for (const r of review['溜まりかけ'] || review.growing || []) push(r.sig, r.count, null);
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 送る(★1日1回)
|
|
185
|
+
* @returns {Promise<{sent:boolean, reason:string, count:number}>}
|
|
186
|
+
*/
|
|
187
|
+
async function reportStats(version, review) {
|
|
188
|
+
const { key, url, anonKey } = config();
|
|
189
|
+
if (!key || !url || !anonKey) return { sent: false, reason: 'unconfigured', count: 0 };
|
|
190
|
+
|
|
191
|
+
if (!isSharing()) return { sent: false, reason: 'opted-out', count: 0 };
|
|
192
|
+
|
|
193
|
+
const stats = buildStats(review);
|
|
194
|
+
if (!stats.length) return { sent: false, reason: 'nothing', count: 0 };
|
|
195
|
+
|
|
196
|
+
// ★1日1回(同じ日に何度起動しても増えない)
|
|
197
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
198
|
+
let cache = null;
|
|
199
|
+
try { cache = JSON.parse(fs.readFileSync(STATS_CACHE, 'utf8')); } catch (_) {}
|
|
200
|
+
if (cache && cache.on === today) return { sent: false, reason: 'already-today', count: 0 };
|
|
201
|
+
|
|
202
|
+
const keyHash = crypto.createHash('sha256').update(key).digest('hex');
|
|
203
|
+
const rpc = url.replace(/verify_license\/?$/, 'report_stats');
|
|
204
|
+
try {
|
|
205
|
+
await post(rpc, { p_key_hash: keyHash, p_version: version || null, p_stats: stats }, anonKey);
|
|
206
|
+
try { fs.writeFileSync(STATS_CACHE, JSON.stringify({ on: today, count: stats.length }, null, 2)); } catch (_) {}
|
|
207
|
+
return { sent: true, reason: 'ok', count: stats.length };
|
|
208
|
+
} catch (e) {
|
|
209
|
+
// ★送れなくても止めない(相手の仕事が止まる理由にしない)
|
|
210
|
+
return { sent: false, reason: 'offline', count: 0 };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = { check, reportStats, buildStats, isSharing, setSharing };
|
package/package.json
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@horizon_works/banto",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AI番頭のOS。ルール・記憶・検査を1本のMCPで配ります。中身をクラウドへ渡さずに検査できます。",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"mcp",
|
|
7
|
-
"model-context-protocol",
|
|
8
|
-
"banto",
|
|
9
|
-
"bantou",
|
|
10
|
-
"ai-agent",
|
|
11
|
-
"privacy",
|
|
12
|
-
"japanese"
|
|
13
|
-
],
|
|
14
|
-
"license": "UNLICENSED",
|
|
15
|
-
"private": false,
|
|
16
|
-
"author": "株式会社ホライズンワークス",
|
|
17
|
-
"type": "commonjs",
|
|
18
|
-
"bin": {
|
|
19
|
-
"banto": "server.js"
|
|
20
|
-
},
|
|
21
|
-
"main": "server.js",
|
|
22
|
-
"engines": {
|
|
23
|
-
"node": ">=18"
|
|
24
|
-
},
|
|
25
|
-
"files": [
|
|
26
|
-
"server.js",
|
|
27
|
-
"gate_core.js",
|
|
28
|
-
"privacy.js",
|
|
29
|
-
"shelf.js",
|
|
30
|
-
"gtasks.js",
|
|
31
|
-
"env.js",
|
|
32
|
-
"subconscious.js",
|
|
33
|
-
"supplements.json",
|
|
34
|
-
"payload/",
|
|
35
|
-
"license/client.js",
|
|
36
|
-
"README.md"
|
|
37
|
-
],
|
|
38
|
-
"publishConfig": {
|
|
39
|
-
"access": "public"
|
|
40
|
-
},
|
|
41
|
-
"optionalDependencies": {
|
|
42
|
-
"googleapis": "^144.0.0",
|
|
43
|
-
"@google-cloud/local-auth": "^3.0.1"
|
|
44
|
-
}
|
|
45
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@horizon_works/banto",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "AI番頭のOS。ルール・記憶・検査を1本のMCPで配ります。中身をクラウドへ渡さずに検査できます。",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"banto",
|
|
9
|
+
"bantou",
|
|
10
|
+
"ai-agent",
|
|
11
|
+
"privacy",
|
|
12
|
+
"japanese"
|
|
13
|
+
],
|
|
14
|
+
"license": "UNLICENSED",
|
|
15
|
+
"private": false,
|
|
16
|
+
"author": "株式会社ホライズンワークス",
|
|
17
|
+
"type": "commonjs",
|
|
18
|
+
"bin": {
|
|
19
|
+
"banto": "server.js"
|
|
20
|
+
},
|
|
21
|
+
"main": "server.js",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"server.js",
|
|
27
|
+
"gate_core.js",
|
|
28
|
+
"privacy.js",
|
|
29
|
+
"shelf.js",
|
|
30
|
+
"gtasks.js",
|
|
31
|
+
"env.js",
|
|
32
|
+
"subconscious.js",
|
|
33
|
+
"supplements.json",
|
|
34
|
+
"payload/",
|
|
35
|
+
"license/client.js",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"optionalDependencies": {
|
|
42
|
+
"googleapis": "^144.0.0",
|
|
43
|
+
"@google-cloud/local-auth": "^3.0.1"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/privacy.js
CHANGED
|
@@ -71,9 +71,52 @@ function setPolicy(dir, level) {
|
|
|
71
71
|
// ───────────────────────────── 伏せる/戻す
|
|
72
72
|
const MAPDIR = '.bantou-mask'; // ★対応表の置き場(手元だけ。AIには渡さない)
|
|
73
73
|
|
|
74
|
+
// 🚨 ★印の種類は、ここが唯一の正本(2026-08-21)。
|
|
75
|
+
// `tel` を足したとき、**数える側の正規表現に足し忘れた**。しかも数える側は2箇所あった。
|
|
76
|
+
// ★同じ辞書を2つ書かない。数える側は、この表から作る。
|
|
77
|
+
const 印の種類 = { '人名': 'name', '法人名': 'corp', '事務所名': 'office', '屋号': 'shop',
|
|
78
|
+
'電話': 'tel', '辞書': 'word' };
|
|
79
|
+
const 印の正規表現 = () =>
|
|
80
|
+
new RegExp('\\b(?:' + [...new Set(Object.values(印の種類))].concat('item').join('|') + ')\\d{3,6}\\b', 'g');
|
|
81
|
+
|
|
74
82
|
function tokenFor(kind, n) {
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
return (印の種類[kind] || 'item') + String(n).padStart(5, '0');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 🚨 ★検出の「当たり付け」を、そのまま置き換えに使わない(2026-08-21 実測で判明)
|
|
87
|
+
//
|
|
88
|
+
// 検出側の法人名パターンは `株式会社` のあとを **12字まで貪欲に**取る。
|
|
89
|
+
// 候補を挙げるだけなら十分だが、**置き換えると文が壊れる**。実際にこうなった:
|
|
90
|
+
// 元 : 株式会社みどり工務店の締めは20日。
|
|
91
|
+
// 伏せ: name00001日。 ← ★「の締めは20」まで食べた
|
|
92
|
+
//
|
|
93
|
+
// ★「見つける」と「置き換える」では、要る精度が違う。同じ形を使い回さない。
|
|
94
|
+
const 助詞など = /[のはがをにでともへやかなど、。「」『』()()\s]/;
|
|
95
|
+
|
|
96
|
+
// 先に付く法人格(★これは「語尾」ではないので、ここで切ってはいけない)
|
|
97
|
+
const 法人格 = /^(?:株式会社|合同会社|有限会社|一般社団法人|一般財団法人|公益社団法人|公益財団法人|特定非営利活動法人|社会福祉法人|医療法人|学校法人|宗教法人|社労士法人|税理士法人)/;
|
|
98
|
+
// うしろに付く語尾(★いちばん後ろまで取る=貪欲)
|
|
99
|
+
const 語尾 = /^(.*(?:株式会社|工務店|商店|工業所|製作所|鉄工所|事務所|法人|会|園|店))/;
|
|
100
|
+
|
|
101
|
+
function 実体だけにする(s) {
|
|
102
|
+
const t = String(s);
|
|
103
|
+
// 🚨 最初に書いた `^(.*?(?:株式会社|…))` は、**先頭の「株式会社」自身で止まった**
|
|
104
|
+
// (非貪欲なので最初の一致で終わる)。結果「株式会社」だけを伏せて社名が残った。
|
|
105
|
+
// ★頭と尻は別に扱う。
|
|
106
|
+
const 頭 = (t.match(法人格) || [''])[0];
|
|
107
|
+
const 残り = t.slice(頭.length);
|
|
108
|
+
const m = 残り.match(語尾);
|
|
109
|
+
if (m && m[1]) return 頭 + m[1];
|
|
110
|
+
const i = 残り.split('').findIndex((c) => 助詞など.test(c) || /[0-90-9]/.test(c));
|
|
111
|
+
return 頭 + (i > 0 ? 残り.slice(0, i) : 残り);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function 種別(s) {
|
|
115
|
+
const t = String(s);
|
|
116
|
+
if (/(株式会社|合同会社|有限会社|社労士法人|税理士法人|医療法人|学校法人)/.test(t)) return '法人名';
|
|
117
|
+
if (/(事務所)$/.test(t)) return '事務所名';
|
|
118
|
+
if (/(工務店|商店|工業所|製作所|鉄工所)$/.test(t)) return '屋号';
|
|
119
|
+
return '人名';
|
|
77
120
|
}
|
|
78
121
|
|
|
79
122
|
/**
|
|
@@ -85,6 +128,7 @@ function maskFile(FREE, target, opts) {
|
|
|
85
128
|
if (!fs.existsSync(src)) throw new Error('見つかりません: ' + src);
|
|
86
129
|
const text = fs.readFileSync(src, 'utf8');
|
|
87
130
|
const dir = path.dirname(src);
|
|
131
|
+
// (実体だけにする/種別 は下に定義)
|
|
88
132
|
|
|
89
133
|
const always = scanAlways(text);
|
|
90
134
|
const dict = FREE.loadDict();
|
|
@@ -93,7 +137,9 @@ function maskFile(FREE, target, opts) {
|
|
|
93
137
|
// 伏せる対象=辞書の語 + 辞書に無い固有名の候補(★長いものから置換。部分一致で壊さない)
|
|
94
138
|
const targets = [];
|
|
95
139
|
for (const h of found.dictHits) targets.push({ text: h.word, kind: '辞書' });
|
|
96
|
-
for (const c of found.candidates) targets.push({ text: c.text, kind:
|
|
140
|
+
for (const c of found.candidates) targets.push({ text: 実体だけにする(c.text), kind: 種別(c.text) });
|
|
141
|
+
// ★形で分かるもの(電話番号)も伏せる。辞書が無くても効く
|
|
142
|
+
for (const t of (text.match(/0\d{1,4}[-(]?\d{1,4}[-)]?\d{3,4}/g) || [])) targets.push({ text: t, kind: '電話' });
|
|
97
143
|
targets.sort((a, b) => b.text.length - a.text.length);
|
|
98
144
|
|
|
99
145
|
const map = {}; // token → 実物
|
|
@@ -104,10 +150,16 @@ function maskFile(FREE, target, opts) {
|
|
|
104
150
|
if (rev[t.text]) continue;
|
|
105
151
|
n++;
|
|
106
152
|
const tok = tokenFor(t.kind, n);
|
|
153
|
+
// 🚨 ★置き換わらなかったものを、印として台帳に載せない(2026-08-21 実測)
|
|
154
|
+
// 長い語を先に置換するので、短い語(「株式会社」だけ等)は本文から消えている。
|
|
155
|
+
// それでも印を作ると、戻すときに「**使われなかった印がある**」と鳴って往復が止まる。
|
|
156
|
+
// ★「作った数」ではなく「**実際に置き換わった数**」を数える。
|
|
157
|
+
const 置換後 = out.split(t.text).join(tok);
|
|
158
|
+
if (置換後 === out) { n--; continue; }
|
|
107
159
|
rev[t.text] = tok;
|
|
108
160
|
map[tok] = t.text;
|
|
109
161
|
byKind[t.kind] = (byKind[t.kind] || 0) + 1;
|
|
110
|
-
out =
|
|
162
|
+
out = 置換後;
|
|
111
163
|
}
|
|
112
164
|
|
|
113
165
|
// ★絶対層は「伏せる」ではなく「消す」。戻す必要が無いものを持ち回らない
|
|
@@ -128,7 +180,11 @@ function maskFile(FREE, target, opts) {
|
|
|
128
180
|
fs.writeFileSync(masked, '<!-- bantou-mask:' + id + ' -->\n' + out, 'utf8');
|
|
129
181
|
|
|
130
182
|
return {
|
|
131
|
-
|
|
183
|
+
// 🚨 ★フルパスを返さない(2026-08-22 新しい関所が捕まえた)。
|
|
184
|
+
// Windows の道は C:\Users\<利用者名>\… なので、**利用者本人の名前がAIへ渡る**。
|
|
185
|
+
// ★AIは元のファイルの場所を自分で渡しているので、**ファイル名だけで足りる**。
|
|
186
|
+
伏せた版: path.basename(masked),
|
|
187
|
+
場所: '★お渡しいただいた正本と同じフォルダです',
|
|
132
188
|
伏せた語の数: n,
|
|
133
189
|
種別ごとの数: byKind, // ★数だけ。語は返さない
|
|
134
190
|
削除した機密: always.length ? always : undefined, // ★種別と数だけ
|
|
@@ -158,7 +214,8 @@ function unmaskFile(FREE, maskedPath, outPath, force) {
|
|
|
158
214
|
const body = text.replace(/<!--\s*bantou-mask:[0-9a-f]+\s*-->\n?/, '');
|
|
159
215
|
|
|
160
216
|
// ① 表に無いトークンが混ざっていないか(★AIが壊した/作った)
|
|
161
|
-
|
|
217
|
+
// ★印の種類は 印の種類 表から作る(種類を足したら、ここは自動で追従する)
|
|
218
|
+
const used = body.match(印の正規表現()) || [];
|
|
162
219
|
const unknown = [...new Set(used.filter((t) => !map[t]))];
|
|
163
220
|
|
|
164
221
|
// ② 使われなかったトークン(★AIが消した)
|
|
@@ -168,7 +225,13 @@ function unmaskFile(FREE, maskedPath, outPath, force) {
|
|
|
168
225
|
const dict = FREE.loadDict();
|
|
169
226
|
const restored = Object.entries(map).reduce((s, [t, v]) => s.split(t).join(v), body);
|
|
170
227
|
const found = FREE.inspectText(restored, dict);
|
|
171
|
-
|
|
228
|
+
// 🚨 ★完全一致で照らすと、同じ語の一部が「新しい名前」に見える(2026-08-21 実測)
|
|
229
|
+
// 例=伏せたのは「株式会社みどり工務店」、検出の当たり付けは「みどり」を返す。
|
|
230
|
+
// 別物ではないのに、**戻す関所が毎回鳴って往復が止まった**。
|
|
231
|
+
// ★どちらかがどちらかを含んでいれば、同じものとして扱う。
|
|
232
|
+
const 伏せた語 = Object.values(map);
|
|
233
|
+
const newNames = found.candidates.filter((c) =>
|
|
234
|
+
!伏せた語.some((v) => v === c.text || v.includes(c.text) || c.text.includes(v)));
|
|
172
235
|
|
|
173
236
|
const problems = [];
|
|
174
237
|
if (unknown.length) problems.push('表に無い印が ' + unknown.length + ' 種あります(AIが書き換えたか、作ったものです)');
|
|
@@ -199,7 +262,8 @@ function unmaskFile(FREE, maskedPath, outPath, force) {
|
|
|
199
262
|
fs.writeFileSync(dst, restored, 'utf8');
|
|
200
263
|
return {
|
|
201
264
|
書きました: true,
|
|
202
|
-
書き先: dst,
|
|
265
|
+
書き先: path.basename(dst), // ★フルパスを返さない(利用者名が入るため)
|
|
266
|
+
場所: '★お渡しいただいた正本と同じフォルダです',
|
|
203
267
|
戻した印の数: Object.keys(map).length - missing.length,
|
|
204
268
|
注意: problems.length ? problems : undefined,
|
|
205
269
|
note: '★元のファイルは .bak として残しています。中身はAIに渡していません。ご自身で開いて確かめてください。',
|
|
@@ -217,7 +281,8 @@ function render(FREE, maskId, text, outPath, dir) {
|
|
|
217
281
|
if (!fs.existsSync(mapFile)) throw new Error('対応表が見つかりません: ' + mapFile);
|
|
218
282
|
const { map } = JSON.parse(fs.readFileSync(mapFile, 'utf8'));
|
|
219
283
|
const body = String(text || '');
|
|
220
|
-
|
|
284
|
+
// ★印の種類は 印の種類 表から作る(種類を足したら、ここは自動で追従する)
|
|
285
|
+
const used = body.match(印の正規表現()) || [];
|
|
221
286
|
const unknown = [...new Set(used.filter((t) => !map[t]))];
|
|
222
287
|
const restored = Object.entries(map).reduce((s, [t, v]) => s.split(t).join(v), body);
|
|
223
288
|
|
package/server.js
CHANGED
|
@@ -162,8 +162,11 @@ function inspect(targetPath) {
|
|
|
162
162
|
},
|
|
163
163
|
verdict: reasons.length ? '要確認' : 'この2つの検査では検出なし',
|
|
164
164
|
verdict_reasons: reasons, // ★何で引っかかったかの「系統」。語は含まない
|
|
165
|
+
// 🚨 ★フルパスを返さない(2026-08-21 実測で判明。フリー版と同じ直し)
|
|
166
|
+
// 一時フォルダの道に **利用者本人の名前** が入っていた(C:\Users\<名前>\…)。
|
|
165
167
|
detail_report: reportPath
|
|
166
|
-
? '
|
|
168
|
+
? '詳細は、お使いのパソコンの一時フォルダ(Windowsなら %TEMP%)の '
|
|
169
|
+
+ path.basename(reportPath) + ' に書きました。★人が手元で開いてください(AIは読みません)'
|
|
167
170
|
: null,
|
|
168
171
|
note:
|
|
169
172
|
'この結果は「この2つの検査で引っかかった数」です。辞書に無い綴りは辞書側では永遠に0件になります。' +
|
|
@@ -263,6 +266,19 @@ const TOOLS = [
|
|
|
263
266
|
},
|
|
264
267
|
},
|
|
265
268
|
},
|
|
269
|
+
{
|
|
270
|
+
name: 'share_stats',
|
|
271
|
+
description:
|
|
272
|
+
'★型キーの統計を、番頭を作った側(ホライズンワークス)へ送るかどうかを決めます。**既定は送りません**。' +
|
|
273
|
+
'送るのは「型キーの名前と件数」だけで、記録の本文・顧客名・文章は一切送りません。' +
|
|
274
|
+
'★呼ぶと、いま実際に送られる中身がそのまま見えます。いつでも止められます。',
|
|
275
|
+
inputSchema: {
|
|
276
|
+
type: 'object',
|
|
277
|
+
properties: {
|
|
278
|
+
on: { type: 'boolean', description: 'true で送る/false で止める。省略すると今の設定と中身を見せるだけ' },
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
266
282
|
{
|
|
267
283
|
name: 'review',
|
|
268
284
|
description:
|
|
@@ -1427,6 +1443,28 @@ function callTool(name, args) {
|
|
|
1427
1443
|
|
|
1428
1444
|
if (name === 'review') return FREE.review(true);
|
|
1429
1445
|
|
|
1446
|
+
// ★型キーの統計を送るかどうか(既定オフのオプトイン)
|
|
1447
|
+
// ★この道具は「切り替え」と「中身を見せる」を必ず同時にやる。
|
|
1448
|
+
// 何が送られるか見えない同意は、同意ではないため。
|
|
1449
|
+
if (name === 'share_stats') {
|
|
1450
|
+
const LIC = (() => { try { return require('./license/client.js'); } catch (_) { return null; } })();
|
|
1451
|
+
if (!LIC || !LIC.setSharing) return { error: 'この構成では使えません(鍵の口がありません)' };
|
|
1452
|
+
if (args && typeof args.on === 'boolean') LIC.setSharing(args.on);
|
|
1453
|
+
const on = LIC.isSharing();
|
|
1454
|
+
const 送られるもの = LIC.buildStats(FREE.review(false));
|
|
1455
|
+
return {
|
|
1456
|
+
いまの設定: on ? '送ります' : '★送りません(既定)',
|
|
1457
|
+
送るもの: '型キーの名前と件数だけ',
|
|
1458
|
+
送らないもの: ['記録の本文', '検査した文章', '顧客名・事業所名', 'ファイルの中身', 'パソコンの中の何か'],
|
|
1459
|
+
回数: '1日に1回まで',
|
|
1460
|
+
'★いま送られる中身': 送られるもの.length ? 送られるもの : '(まだ型キーが付いた記録がありません)',
|
|
1461
|
+
切り替え方: 'share_stats を on: true / false で呼んでください。いつでも変えられます',
|
|
1462
|
+
note: on
|
|
1463
|
+
? '★これは、同じ失敗が他の方でも起きているかを見て、助言に使うためのものです。中身は読めません。'
|
|
1464
|
+
: '★送らない設定のままでも、番頭の機能はすべて使えます。',
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1430
1468
|
if (name === 'settle') {
|
|
1431
1469
|
return FREE.settle(args && args.ids, args && args.sig, args && args.ruled_to);
|
|
1432
1470
|
}
|
|
@@ -1637,13 +1675,23 @@ async function handle(req) {
|
|
|
1637
1675
|
let LICENSE_STATE = { allowed: true, status: 'unconfigured', message: '', daysLeft: null };
|
|
1638
1676
|
(async () => {
|
|
1639
1677
|
try {
|
|
1640
|
-
const
|
|
1641
|
-
LICENSE_STATE = await
|
|
1678
|
+
const LIC = require('./license/client.js');
|
|
1679
|
+
LICENSE_STATE = await check_(LIC);
|
|
1680
|
+
|
|
1681
|
+
// ★型キーの統計(段1)— **同意しているときだけ**。1日1回。
|
|
1682
|
+
// ここで送るのは型キーの名前と件数だけ(組み立ては client 側の buildStats に閉じてある)。
|
|
1683
|
+
// ★送れなくても、何も止めない。
|
|
1684
|
+
if (LICENSE_STATE.allowed && LIC.isSharing && LIC.isSharing()) {
|
|
1685
|
+
try { await LIC.reportStats(VERSION, FREE.review(false)); } catch (_) {}
|
|
1686
|
+
}
|
|
1642
1687
|
} catch (_) {
|
|
1643
1688
|
// license モジュールが無い構成でも動く
|
|
1644
1689
|
}
|
|
1645
1690
|
})();
|
|
1646
1691
|
|
|
1692
|
+
// ★版番号を固定で書かない(0.1.0 のままだった=どの版が動いているか台帳で分からなくなる)
|
|
1693
|
+
async function check_(LIC) { return LIC.check(VERSION); }
|
|
1694
|
+
|
|
1647
1695
|
// ★1つずつ順に処理する(2026-08-21)。
|
|
1648
1696
|
// 待てる形(async)にした途端、**要求が同時に走る**ようになり、
|
|
1649
1697
|
// 「入れる」の途中で「一覧」が走って『未導入』と返った。
|