taiwan-invoice-skill 2.1.0 → 2.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/README.md +17 -0
- package/assets/taiwan-invoice/SKILL.md +485 -0
- package/assets/taiwan-invoice/data/error-codes.csv +41 -0
- package/assets/taiwan-invoice/data/field-mappings.csv +27 -0
- package/assets/taiwan-invoice/data/operations.csv +11 -0
- package/assets/taiwan-invoice/data/providers.csv +4 -0
- package/assets/taiwan-invoice/data/reasoning.csv +32 -0
- package/assets/taiwan-invoice/data/tax-rules.csv +9 -0
- package/assets/taiwan-invoice/data/troubleshooting.csv +17 -0
- package/assets/taiwan-invoice/scripts/__pycache__/core.cpython-312.pyc +0 -0
- package/assets/taiwan-invoice/scripts/core.py +310 -0
- package/assets/taiwan-invoice/scripts/generate-invoice-service.py +642 -128
- package/assets/taiwan-invoice/scripts/persist.py +330 -0
- package/assets/taiwan-invoice/scripts/recommend.py +373 -0
- package/assets/taiwan-invoice/scripts/search.py +273 -0
- package/dist/index.js +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Taiwan Invoice Skill - Search CLI
|
|
4
|
+
電子發票智能搜索命令行工具
|
|
5
|
+
|
|
6
|
+
用法:
|
|
7
|
+
python search.py "ecpay B2C" --domain operation
|
|
8
|
+
python search.py "1999 error" --domain error
|
|
9
|
+
python search.py "稅額計算" --domain tax
|
|
10
|
+
python search.py "綠界" --all
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import sys
|
|
15
|
+
from typing import Dict, List, Any
|
|
16
|
+
|
|
17
|
+
from core import (
|
|
18
|
+
search,
|
|
19
|
+
search_all,
|
|
20
|
+
detect_domain,
|
|
21
|
+
get_available_domains,
|
|
22
|
+
get_domain_info
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def format_ascii_box(title: str, content: List[str], width: int = 80, style: str = 'double') -> str:
|
|
27
|
+
"""
|
|
28
|
+
格式化 ASCII Box 輸出
|
|
29
|
+
|
|
30
|
+
style: 'single' (─│) or 'double' (═║)
|
|
31
|
+
"""
|
|
32
|
+
if style == 'double':
|
|
33
|
+
h, v, tl, tr, bl, br, lm, rm = '═', '║', '╔', '╗', '╚', '╝', '╠', '╣'
|
|
34
|
+
else:
|
|
35
|
+
h, v, tl, tr, bl, br, lm, rm = '─', '│', '┌', '┐', '└', '┘', '├', '┤'
|
|
36
|
+
|
|
37
|
+
lines = []
|
|
38
|
+
lines.append(tl + h * (width - 2) + tr)
|
|
39
|
+
lines.append(v + f' {title}'.ljust(width - 2) + v)
|
|
40
|
+
lines.append(lm + h * (width - 2) + rm)
|
|
41
|
+
|
|
42
|
+
for line in content:
|
|
43
|
+
# 處理過長的行
|
|
44
|
+
if len(line) > width - 4:
|
|
45
|
+
line = line[:width - 7] + '...'
|
|
46
|
+
lines.append(v + ' ' + line.ljust(width - 4) + ' ' + v)
|
|
47
|
+
|
|
48
|
+
lines.append(bl + h * (width - 2) + br)
|
|
49
|
+
return '\n'.join(lines)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def format_result(result: Dict[str, Any], domain: str) -> List[str]:
|
|
53
|
+
"""
|
|
54
|
+
格式化單個搜索結果
|
|
55
|
+
"""
|
|
56
|
+
lines = []
|
|
57
|
+
score = result.get('_score', 0)
|
|
58
|
+
lines.append(f'Score: {score}')
|
|
59
|
+
lines.append('')
|
|
60
|
+
|
|
61
|
+
for key, value in result.items():
|
|
62
|
+
if key == '_score' or not value:
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
# 截斷過長的值
|
|
66
|
+
value_str = str(value)
|
|
67
|
+
if len(value_str) > 60:
|
|
68
|
+
value_str = value_str[:57] + '...'
|
|
69
|
+
|
|
70
|
+
lines.append(f'{key}: {value_str}')
|
|
71
|
+
|
|
72
|
+
return lines
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def format_domain_results(results: List[Dict[str, Any]], domain: str, query: str) -> str:
|
|
76
|
+
"""
|
|
77
|
+
格式化整個域的搜索結果
|
|
78
|
+
"""
|
|
79
|
+
if not results:
|
|
80
|
+
return f"No results found in '{domain}' for query: {query}"
|
|
81
|
+
|
|
82
|
+
output = []
|
|
83
|
+
output.append(f"\n{'='*60}")
|
|
84
|
+
output.append(f"Domain: {domain.upper()} | Query: {query} | Results: {len(results)}")
|
|
85
|
+
output.append('='*60)
|
|
86
|
+
|
|
87
|
+
for i, result in enumerate(results, 1):
|
|
88
|
+
lines = format_result(result, domain)
|
|
89
|
+
box = format_ascii_box(f'Result {i}', lines, width=60)
|
|
90
|
+
output.append(box)
|
|
91
|
+
output.append('')
|
|
92
|
+
|
|
93
|
+
return '\n'.join(output)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def format_all_results(results: Dict[str, List[Dict[str, Any]]], query: str) -> str:
|
|
97
|
+
"""
|
|
98
|
+
格式化所有域的搜索結果
|
|
99
|
+
"""
|
|
100
|
+
if not results:
|
|
101
|
+
return f"No results found for query: {query}"
|
|
102
|
+
|
|
103
|
+
output = []
|
|
104
|
+
output.append(f"\n{'#'*60}")
|
|
105
|
+
output.append(f"# SEARCH ALL DOMAINS: {query}")
|
|
106
|
+
output.append('#'*60)
|
|
107
|
+
|
|
108
|
+
for domain, domain_results in results.items():
|
|
109
|
+
output.append(format_domain_results(domain_results, domain, query))
|
|
110
|
+
|
|
111
|
+
return '\n'.join(output)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def format_markdown_result(result: Dict[str, Any], index: int) -> str:
|
|
115
|
+
"""
|
|
116
|
+
格式化單個結果為 Markdown
|
|
117
|
+
"""
|
|
118
|
+
lines = []
|
|
119
|
+
score = result.get('_score', 0)
|
|
120
|
+
lines.append(f"### Result {index} (Score: {score})")
|
|
121
|
+
lines.append("")
|
|
122
|
+
|
|
123
|
+
for key, value in result.items():
|
|
124
|
+
if key == '_score' or not value:
|
|
125
|
+
continue
|
|
126
|
+
lines.append(f"- **{key}**: {value}")
|
|
127
|
+
|
|
128
|
+
lines.append("")
|
|
129
|
+
return '\n'.join(lines)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def format_markdown_domain(results: List[Dict[str, Any]], domain: str, query: str) -> str:
|
|
133
|
+
"""
|
|
134
|
+
格式化域結果為 Markdown
|
|
135
|
+
"""
|
|
136
|
+
if not results:
|
|
137
|
+
return f"No results found in '{domain}' for query: {query}\n"
|
|
138
|
+
|
|
139
|
+
lines = []
|
|
140
|
+
lines.append(f"## {domain.upper()}")
|
|
141
|
+
lines.append(f"")
|
|
142
|
+
lines.append(f"> Query: `{query}` | Results: {len(results)}")
|
|
143
|
+
lines.append("")
|
|
144
|
+
|
|
145
|
+
for i, result in enumerate(results, 1):
|
|
146
|
+
lines.append(format_markdown_result(result, i))
|
|
147
|
+
|
|
148
|
+
return '\n'.join(lines)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def format_markdown_all(results: Dict[str, List[Dict[str, Any]]], query: str) -> str:
|
|
152
|
+
"""
|
|
153
|
+
格式化所有域結果為 Markdown
|
|
154
|
+
"""
|
|
155
|
+
if not results:
|
|
156
|
+
return f"# No results found for query: {query}\n"
|
|
157
|
+
|
|
158
|
+
lines = []
|
|
159
|
+
lines.append(f"# Taiwan Invoice Search Results")
|
|
160
|
+
lines.append(f"")
|
|
161
|
+
lines.append(f"**Query**: `{query}`")
|
|
162
|
+
lines.append(f"")
|
|
163
|
+
lines.append("---")
|
|
164
|
+
lines.append("")
|
|
165
|
+
|
|
166
|
+
for domain, domain_results in results.items():
|
|
167
|
+
lines.append(format_markdown_domain(domain_results, domain, query))
|
|
168
|
+
lines.append("---")
|
|
169
|
+
lines.append("")
|
|
170
|
+
|
|
171
|
+
return '\n'.join(lines)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def list_domains():
|
|
175
|
+
"""
|
|
176
|
+
列出所有可用的搜索域
|
|
177
|
+
"""
|
|
178
|
+
print("\n可用的搜索域 (Available Domains):")
|
|
179
|
+
print("="*50)
|
|
180
|
+
|
|
181
|
+
for domain in get_available_domains():
|
|
182
|
+
info = get_domain_info(domain)
|
|
183
|
+
if info:
|
|
184
|
+
print(f"\n {domain}")
|
|
185
|
+
print(f" 檔案: {info['file']}")
|
|
186
|
+
print(f" 記錄數: {info['total_records']}")
|
|
187
|
+
print(f" 搜索欄位: {', '.join(info['search_cols'])}")
|
|
188
|
+
|
|
189
|
+
print()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def main():
|
|
193
|
+
parser = argparse.ArgumentParser(
|
|
194
|
+
description='Taiwan Invoice Skill - BM25 Search Engine',
|
|
195
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
196
|
+
epilog="""
|
|
197
|
+
Examples:
|
|
198
|
+
python search.py "ecpay B2C" # Auto-detect domain
|
|
199
|
+
python search.py "開立發票" --domain operation # Search operations
|
|
200
|
+
python search.py "10000016" --domain error # Search error codes
|
|
201
|
+
python search.py "統編" --domain field # Search field mappings
|
|
202
|
+
python search.py "B2B 稅額" --domain tax # Search tax rules
|
|
203
|
+
python search.py "列印空白" --domain troubleshoot # Search troubleshooting
|
|
204
|
+
python search.py "ECPay" --all # Search all domains
|
|
205
|
+
python search.py --list # List available domains
|
|
206
|
+
"""
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
parser.add_argument('query', nargs='?', help='Search query')
|
|
210
|
+
parser.add_argument('-d', '--domain', choices=get_available_domains(),
|
|
211
|
+
help='Search domain (auto-detect if not specified)')
|
|
212
|
+
parser.add_argument('-n', '--max-results', type=int, default=5,
|
|
213
|
+
help='Maximum results per domain (default: 5)')
|
|
214
|
+
parser.add_argument('-a', '--all', action='store_true',
|
|
215
|
+
help='Search all domains')
|
|
216
|
+
parser.add_argument('-l', '--list', action='store_true',
|
|
217
|
+
help='List available domains')
|
|
218
|
+
parser.add_argument('-f', '--format', choices=['ascii', 'simple', 'json', 'markdown', 'md'],
|
|
219
|
+
default='ascii', help='Output format (default: ascii)')
|
|
220
|
+
|
|
221
|
+
args = parser.parse_args()
|
|
222
|
+
|
|
223
|
+
# 列出域
|
|
224
|
+
if args.list:
|
|
225
|
+
list_domains()
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
# 檢查查詢
|
|
229
|
+
if not args.query:
|
|
230
|
+
parser.print_help()
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
query = args.query
|
|
234
|
+
|
|
235
|
+
# 搜索所有域
|
|
236
|
+
if args.all:
|
|
237
|
+
results = search_all(query, args.max_results)
|
|
238
|
+
|
|
239
|
+
if args.format == 'json':
|
|
240
|
+
import json
|
|
241
|
+
print(json.dumps(results, ensure_ascii=False, indent=2))
|
|
242
|
+
elif args.format in ('markdown', 'md'):
|
|
243
|
+
print(format_markdown_all(results, query))
|
|
244
|
+
else:
|
|
245
|
+
print(format_all_results(results, query))
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
# 單域搜索
|
|
249
|
+
domain = args.domain
|
|
250
|
+
if not domain:
|
|
251
|
+
domain = detect_domain(query)
|
|
252
|
+
if args.format not in ('json', 'markdown', 'md'):
|
|
253
|
+
print(f"[Auto-detected domain: {domain}]")
|
|
254
|
+
|
|
255
|
+
results = search(query, domain, args.max_results)
|
|
256
|
+
|
|
257
|
+
if args.format == 'json':
|
|
258
|
+
import json
|
|
259
|
+
print(json.dumps(results, ensure_ascii=False, indent=2))
|
|
260
|
+
elif args.format in ('markdown', 'md'):
|
|
261
|
+
print(format_markdown_domain(results, domain, query))
|
|
262
|
+
elif args.format == 'simple':
|
|
263
|
+
for i, result in enumerate(results, 1):
|
|
264
|
+
print(f"\n[{i}] Score: {result.get('_score', 0)}")
|
|
265
|
+
for key, value in result.items():
|
|
266
|
+
if key != '_score' and value:
|
|
267
|
+
print(f" {key}: {value}")
|
|
268
|
+
else:
|
|
269
|
+
print(format_domain_results(results, domain, query))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
if __name__ == '__main__':
|
|
273
|
+
main()
|
package/dist/index.js
CHANGED
|
@@ -15087,6 +15087,11 @@ async function copyTaiwanInvoiceAssets(targetSkillDir, sections) {
|
|
|
15087
15087
|
await (0, import_promises.mkdir)(scriptsTarget, { recursive: true });
|
|
15088
15088
|
await (0, import_promises.cp)((0, import_node_path.join)(sourceDir, "scripts"), scriptsTarget, { recursive: true });
|
|
15089
15089
|
}
|
|
15090
|
+
if (sections.scripts && await exists((0, import_node_path.join)(sourceDir, "data"))) {
|
|
15091
|
+
const dataTarget = (0, import_node_path.join)(targetSkillDir, "data");
|
|
15092
|
+
await (0, import_promises.mkdir)(dataTarget, { recursive: true });
|
|
15093
|
+
await (0, import_promises.cp)((0, import_node_path.join)(sourceDir, "data"), dataTarget, { recursive: true });
|
|
15094
|
+
}
|
|
15090
15095
|
}
|
|
15091
15096
|
async function generatePlatformFiles(targetDir, aiType) {
|
|
15092
15097
|
const config = await loadPlatformConfig(aiType);
|