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.
@@ -0,0 +1,330 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Taiwan Invoice Skill - 持久化模式
4
+ 將發票配置保存為 MASTER.md,供 AI 助手持續參考
5
+
6
+ 用法:
7
+ python persist.py init ECPay # 初始化 ECPay 配置
8
+ python persist.py init SmilePay # 初始化 SmilePay 配置
9
+ python persist.py show # 顯示當前配置
10
+ python persist.py update --key xxx # 更新配置
11
+ """
12
+
13
+ import os
14
+ import sys
15
+ import argparse
16
+ from datetime import datetime
17
+ from typing import Dict, Any, Optional
18
+
19
+ # 取得 data 目錄路徑
20
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
21
+ DATA_DIR = os.path.join(os.path.dirname(SCRIPT_DIR), 'data')
22
+
23
+ # 預設配置目錄
24
+ DEFAULT_CONFIG_DIR = 'invoice-config'
25
+ MASTER_FILENAME = 'MASTER.md'
26
+
27
+ # 服務商配置模板
28
+ PROVIDER_CONFIGS = {
29
+ 'ECPay': {
30
+ 'display_name': '綠界科技',
31
+ 'auth_method': 'AES-128-CBC',
32
+ 'test_url': 'https://einvoice-stage.ecpay.com.tw',
33
+ 'prod_url': 'https://einvoice.ecpay.com.tw',
34
+ 'test_merchant_id': '2000132',
35
+ 'test_hash_key': 'ejCk326UnaZWKisg',
36
+ 'test_hash_iv': 'q9jcZX8Ib9LM8wYk',
37
+ 'endpoints': {
38
+ 'issue_b2c': '/B2CInvoice/Issue',
39
+ 'issue_b2b': '/B2BInvoice/Issue',
40
+ 'void': '/Invoice/IssueInvalid',
41
+ 'allowance': '/Invoice/AllowanceByCollegiate',
42
+ 'print': '/Invoice/Print',
43
+ },
44
+ 'encryption': 'URL Encode → AES-128-CBC → Base64',
45
+ },
46
+ 'SmilePay': {
47
+ 'display_name': '速買配',
48
+ 'auth_method': 'Verify_key',
49
+ 'test_url': 'https://ssl.smse.com.tw/api_test',
50
+ 'prod_url': 'https://ssl.smse.com.tw/api',
51
+ 'test_merchant_id': 'SEI1000034',
52
+ 'test_hash_key': '9D73935693EE0237FABA6AB744E48661',
53
+ 'test_hash_iv': '',
54
+ 'endpoints': {
55
+ 'issue': '/SPEinvoice_Storage.asp',
56
+ 'void': '/SPEinvoice_Invalid.asp',
57
+ 'allowance': '/SPEinvoice_AllowanceByCollegiate.asp',
58
+ 'print': '/SPEinvoice_Print_Single.asp',
59
+ },
60
+ 'encryption': 'URL Parameters + Verify_key',
61
+ },
62
+ 'Amego': {
63
+ 'display_name': '光貿科技',
64
+ 'auth_method': 'MD5 Signature',
65
+ 'test_url': 'https://invoice-api.amego.tw',
66
+ 'prod_url': 'https://invoice-api.amego.tw',
67
+ 'test_merchant_id': '12345678',
68
+ 'test_hash_key': 'sHeq7t8G1wiQvhAuIM27',
69
+ 'test_hash_iv': '',
70
+ 'endpoints': {
71
+ 'issue': '/api/invoice/issue',
72
+ 'void': '/api/invoice/void',
73
+ 'allowance': '/api/invoice/allowance',
74
+ 'print': '/api/invoice/print',
75
+ },
76
+ 'encryption': 'JSON + MD5(data + time + appKey)',
77
+ },
78
+ }
79
+
80
+
81
+ def generate_master_md(provider: str, project_name: str = '', custom_config: Optional[Dict] = None) -> str:
82
+ """
83
+ 生成 MASTER.md 內容
84
+ """
85
+ if provider not in PROVIDER_CONFIGS:
86
+ raise ValueError(f"Unknown provider: {provider}")
87
+
88
+ config = PROVIDER_CONFIGS[provider].copy()
89
+ if custom_config:
90
+ config.update(custom_config)
91
+
92
+ now = datetime.now().strftime('%Y-%m-%d %H:%M')
93
+
94
+ content = f"""# Taiwan Invoice - 專案配置
95
+
96
+ > 此檔案為 AI 助手的持久化配置,請勿手動刪除
97
+
98
+ ## 基本資訊
99
+
100
+ | 項目 | 值 |
101
+ |------|-----|
102
+ | **專案名稱** | {project_name or '未命名專案'} |
103
+ | **加值中心** | {config['display_name']} ({provider}) |
104
+ | **建立時間** | {now} |
105
+ | **環境** | 測試環境 |
106
+
107
+ ---
108
+
109
+ ## 服務商配置
110
+
111
+ ### {config['display_name']} ({provider})
112
+
113
+ **認證方式**: {config['auth_method']}
114
+
115
+ **API 端點**:
116
+ - 測試: `{config['test_url']}`
117
+ - 正式: `{config['prod_url']}`
118
+
119
+ **測試憑證**:
120
+ ```
121
+ MerchantID: {config['test_merchant_id']}
122
+ HashKey: {config['test_hash_key']}
123
+ HashIV: {config['test_hash_iv'] or 'N/A'}
124
+ ```
125
+
126
+ **加密流程**:
127
+ ```
128
+ {config['encryption']}
129
+ ```
130
+
131
+ ---
132
+
133
+ ## API 端點
134
+
135
+ | 操作 | 端點 |
136
+ |------|------|
137
+ """
138
+
139
+ for op, endpoint in config['endpoints'].items():
140
+ content += f"| {op} | `{endpoint}` |\n"
141
+
142
+ content += f"""
143
+ ---
144
+
145
+ ## 發票類型設定
146
+
147
+ ### B2C (二聯式)
148
+
149
+ - 金額: **含稅價**
150
+ - BuyerIdentifier: `0000000000`
151
+ - TaxAmount: `0`
152
+ - 可使用載具/捐贈
153
+
154
+ ### B2B (三聯式)
155
+
156
+ - 金額: **未稅價**
157
+ - 需填寫統編 (8碼)
158
+ - 需計算稅額: `TaxAmount = round(Total - Total/1.05)`
159
+ - **不可**使用載具/捐贈
160
+
161
+ ---
162
+
163
+ ## 環境變數建議
164
+
165
+ ```env
166
+ # {config['display_name']} 配置
167
+ INVOICE_PROVIDER={provider}
168
+ INVOICE_MERCHANT_ID=
169
+ INVOICE_HASH_KEY=
170
+ INVOICE_HASH_IV=
171
+ INVOICE_ENV=test
172
+ ```
173
+
174
+ ---
175
+
176
+ ## 開發檢查清單
177
+
178
+ - [ ] 設定環境變數
179
+ - [ ] 實作加密/解密函數
180
+ - [ ] 建立 InvoiceService 介面
181
+ - [ ] 實作 {provider}InvoiceService
182
+ - [ ] 處理 B2C/B2B 金額計算差異
183
+ - [ ] 儲存 invoiceProvider 和 randomNumber
184
+ - [ ] 實作錯誤處理與 logger
185
+ - [ ] 測試環境驗證
186
+
187
+ ---
188
+
189
+ ## 注意事項
190
+
191
+ 1. **randomNumber**: 開立成功後務必儲存,列印時需要
192
+ 2. **invoiceProvider**: 開立時儲存使用的服務商,列印時使用
193
+ 3. **時間戳記**: {"ECPay 需在 10 分鐘內" if provider == 'ECPay' else "Amego 需在 60 秒內" if provider == 'Amego' else "注意伺服器時間"}
194
+ 4. **載具與捐贈**: 互斥,不可同時設定
195
+
196
+ ---
197
+
198
+ *Generated by Taiwan Invoice Skill v2.3.0*
199
+ """
200
+
201
+ return content
202
+
203
+
204
+ def init_config(provider: str, target_dir: str, project_name: str = '', force: bool = False) -> str:
205
+ """
206
+ 初始化配置
207
+ """
208
+ config_dir = os.path.join(target_dir, DEFAULT_CONFIG_DIR)
209
+ master_path = os.path.join(config_dir, MASTER_FILENAME)
210
+
211
+ # 檢查是否已存在
212
+ if os.path.exists(master_path) and not force:
213
+ raise FileExistsError(f"Config already exists: {master_path}. Use --force to overwrite.")
214
+
215
+ # 建立目錄
216
+ os.makedirs(config_dir, exist_ok=True)
217
+
218
+ # 生成內容
219
+ content = generate_master_md(provider, project_name)
220
+
221
+ # 寫入檔案
222
+ with open(master_path, 'w', encoding='utf-8') as f:
223
+ f.write(content)
224
+
225
+ return master_path
226
+
227
+
228
+ def show_config(target_dir: str) -> Optional[str]:
229
+ """
230
+ 顯示當前配置
231
+ """
232
+ master_path = os.path.join(target_dir, DEFAULT_CONFIG_DIR, MASTER_FILENAME)
233
+
234
+ if not os.path.exists(master_path):
235
+ return None
236
+
237
+ with open(master_path, 'r', encoding='utf-8') as f:
238
+ return f.read()
239
+
240
+
241
+ def format_ascii_box(title: str, content: str, width: int = 70) -> str:
242
+ """
243
+ 格式化 ASCII Box
244
+ """
245
+ lines = []
246
+ lines.append('╔' + '═' * (width - 2) + '╗')
247
+ lines.append('║' + f' {title}'.center(width - 2) + '║')
248
+ lines.append('╠' + '═' * (width - 2) + '╣')
249
+
250
+ for line in content.split('\n'):
251
+ if len(line) > width - 4:
252
+ line = line[:width - 7] + '...'
253
+ lines.append('║' + ' ' + line.ljust(width - 4) + ' ' + '║')
254
+
255
+ lines.append('╚' + '═' * (width - 2) + '╝')
256
+ return '\n'.join(lines)
257
+
258
+
259
+ def main():
260
+ parser = argparse.ArgumentParser(
261
+ description='Taiwan Invoice - 持久化配置工具',
262
+ formatter_class=argparse.RawDescriptionHelpFormatter,
263
+ epilog="""
264
+ Examples:
265
+ python persist.py init ECPay # 初始化 ECPay 配置
266
+ python persist.py init SmilePay -p "MyProject" # 指定專案名稱
267
+ python persist.py show # 顯示當前配置
268
+ python persist.py init Amego --force # 強制覆蓋
269
+ """
270
+ )
271
+
272
+ subparsers = parser.add_subparsers(dest='command', help='Commands')
273
+
274
+ # init 命令
275
+ init_parser = subparsers.add_parser('init', help='Initialize configuration')
276
+ init_parser.add_argument('provider', choices=['ECPay', 'SmilePay', 'Amego'],
277
+ help='Invoice provider')
278
+ init_parser.add_argument('-p', '--project', default='',
279
+ help='Project name')
280
+ init_parser.add_argument('-d', '--dir', default='.',
281
+ help='Target directory (default: current)')
282
+ init_parser.add_argument('-f', '--force', action='store_true',
283
+ help='Force overwrite existing config')
284
+
285
+ # show 命令
286
+ show_parser = subparsers.add_parser('show', help='Show current configuration')
287
+ show_parser.add_argument('-d', '--dir', default='.',
288
+ help='Target directory (default: current)')
289
+
290
+ # list 命令
291
+ list_parser = subparsers.add_parser('list', help='List available providers')
292
+
293
+ args = parser.parse_args()
294
+
295
+ if args.command == 'init':
296
+ try:
297
+ path = init_config(args.provider, args.dir, args.project, args.force)
298
+ print(format_ascii_box(
299
+ '✓ Configuration Initialized',
300
+ f"Provider: {args.provider}\nPath: {path}\n\nNext: Set your credentials in the MASTER.md file"
301
+ ))
302
+ except FileExistsError as e:
303
+ print(f"Error: {e}")
304
+ sys.exit(1)
305
+ except ValueError as e:
306
+ print(f"Error: {e}")
307
+ sys.exit(1)
308
+
309
+ elif args.command == 'show':
310
+ content = show_config(args.dir)
311
+ if content:
312
+ print(content)
313
+ else:
314
+ print("No configuration found. Run 'python persist.py init <provider>' to create one.")
315
+
316
+ elif args.command == 'list':
317
+ print("\nAvailable Providers:")
318
+ print("=" * 50)
319
+ for provider, config in PROVIDER_CONFIGS.items():
320
+ print(f"\n {provider}")
321
+ print(f" {config['display_name']}")
322
+ print(f" Auth: {config['auth_method']}")
323
+ print()
324
+
325
+ else:
326
+ parser.print_help()
327
+
328
+
329
+ if __name__ == '__main__':
330
+ main()
@@ -0,0 +1,373 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Taiwan Invoice Skill - 加值中心推薦系統
4
+ 基於使用者需求推薦最適合的電子發票加值中心
5
+
6
+ 無外部依賴,純 Python 實現
7
+ """
8
+
9
+ import csv
10
+ import os
11
+ import sys
12
+ import argparse
13
+ from typing import List, Dict, Any, Optional, Tuple
14
+
15
+ # 取得 data 目錄路徑
16
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
17
+ DATA_DIR = os.path.join(os.path.dirname(SCRIPT_DIR), 'data')
18
+
19
+ # 推薦規則定義
20
+ RECOMMENDATION_RULES = {
21
+ # 關鍵字 → (provider, weight, reason)
22
+ '穩定': [('ECPay', 3, '市佔率最高,系統穩定性佳')],
23
+ '市佔': [('ECPay', 3, '台灣電子發票市佔率領先')],
24
+ '文檔': [('ECPay', 2, '提供完整 API 文檔與 SDK')],
25
+ 'sdk': [('ECPay', 2, '官方 SDK 支援多種語言')],
26
+ '高交易量': [('ECPay', 3, '適合高交易量電商')],
27
+ '電商': [('ECPay', 2, '電商整合經驗豐富')],
28
+
29
+ '簡單': [('SmilePay', 3, '整合流程最簡單')],
30
+ '快速': [('SmilePay', 3, '最快速完成整合')],
31
+ '小型': [('SmilePay', 2, '適合小型專案')],
32
+ '測試': [('SmilePay', 2, '測試環境設定簡單')],
33
+ '無加密': [('SmilePay', 3, '無需複雜加密流程')],
34
+ '便宜': [('SmilePay', 2, '費用較低')],
35
+
36
+ 'api': [('Amego', 3, 'MIG 4.0 最新 API 標準')],
37
+ '設計': [('Amego', 2, 'API 設計優良')],
38
+ '新': [('Amego', 2, '採用最新技術標準')],
39
+ 'mig': [('Amego', 3, '完整支援 MIG 4.0 規範')],
40
+ '標準': [('Amego', 2, 'API 設計符合業界標準')],
41
+
42
+ # B2B/B2C 相關
43
+ 'b2b': [('ECPay', 1, 'B2B 發票功能完整'), ('Amego', 1, 'B2B 計算清晰')],
44
+ 'b2c': [('ECPay', 1, 'B2C 市佔最高'), ('SmilePay', 1, 'B2C 整合簡單')],
45
+ '統編': [('ECPay', 1, 'B2B 統編發票經驗豐富')],
46
+
47
+ # 功能相關
48
+ '列印': [('ECPay', 2, '列印功能完整'), ('SmilePay', 1, '支援列印')],
49
+ '作廢': [('ECPay', 1, '作廢流程完整')],
50
+ '折讓': [('ECPay', 1, '折讓功能完整')],
51
+ '載具': [('ECPay', 1, '載具支援完整'), ('SmilePay', 1, '載具整合簡單')],
52
+ '捐贈': [('ECPay', 1, '捐贈功能完整')],
53
+ }
54
+
55
+ # 反模式警告
56
+ ANTI_PATTERNS = {
57
+ 'ECPay': [
58
+ ('無技術資源', '加密流程較複雜,需要一定技術能力'),
59
+ ('極簡整合', '如果只需最簡單整合,SmilePay 可能更適合'),
60
+ ],
61
+ 'SmilePay': [
62
+ ('高交易量', '大型電商建議使用 ECPay 以確保穩定性'),
63
+ ('複雜需求', 'API 功能相對基本,複雜需求可能受限'),
64
+ ('b2b', 'B2B 發票功能較少文檔'),
65
+ ],
66
+ 'Amego': [
67
+ ('市佔', '市佔率相對較低'),
68
+ ('社群', '社群支援與範例相對較少'),
69
+ ('穩定', '如果穩定性是首要考量,ECPay 更保險'),
70
+ ],
71
+ }
72
+
73
+
74
+ def load_providers() -> List[Dict[str, str]]:
75
+ """載入加值中心資料"""
76
+ filepath = os.path.join(DATA_DIR, 'providers.csv')
77
+ if not os.path.exists(filepath):
78
+ return []
79
+
80
+ with open(filepath, 'r', encoding='utf-8') as f:
81
+ reader = csv.DictReader(f)
82
+ return list(reader)
83
+
84
+
85
+ def load_reasoning_rules() -> List[Dict[str, str]]:
86
+ """載入推理規則"""
87
+ filepath = os.path.join(DATA_DIR, 'reasoning.csv')
88
+ if not os.path.exists(filepath):
89
+ return []
90
+
91
+ with open(filepath, 'r', encoding='utf-8') as f:
92
+ reader = csv.DictReader(f)
93
+ return list(reader)
94
+
95
+
96
+ def analyze_requirements(query: str) -> Dict[str, Tuple[int, List[str]]]:
97
+ """
98
+ 分析使用者需求,計算各加值中心分數
99
+
100
+ Returns:
101
+ Dict[provider, (score, reasons)]
102
+ """
103
+ query_lower = query.lower()
104
+
105
+ scores = {
106
+ 'ECPay': (0, []),
107
+ 'SmilePay': (0, []),
108
+ 'Amego': (0, []),
109
+ }
110
+
111
+ # 從 reasoning.csv 載入規則
112
+ reasoning_rules = load_reasoning_rules()
113
+ confidence_weights = {'HIGH': 3, 'MEDIUM': 2, 'LOW': 1}
114
+
115
+ for rule in reasoning_rules:
116
+ scenario = rule.get('scenario', '').lower()
117
+ use_cases = rule.get('use_cases', '').lower()
118
+
119
+ # 檢查場景或使用案例是否匹配查詢
120
+ scenario_words = scenario.replace(' ', '')
121
+ if any(word in query_lower for word in scenario.split()) or \
122
+ any(word in query_lower for word in use_cases.split()):
123
+ provider = rule.get('recommended_provider', '')
124
+ confidence = rule.get('confidence', 'LOW')
125
+ reason = rule.get('reason', '')
126
+
127
+ if provider in scores:
128
+ weight = confidence_weights.get(confidence, 1)
129
+ current_score, reasons = scores[provider]
130
+ if reason and reason not in reasons:
131
+ scores[provider] = (current_score + weight, reasons + [reason])
132
+
133
+ # 根據關鍵字累計分數 (fallback)
134
+ for keyword, rules in RECOMMENDATION_RULES.items():
135
+ if keyword.lower() in query_lower:
136
+ for provider, weight, reason in rules:
137
+ current_score, reasons = scores[provider]
138
+ if reason not in reasons:
139
+ scores[provider] = (current_score + weight, reasons + [reason])
140
+
141
+ return scores
142
+
143
+
144
+ def get_anti_pattern_warnings(query: str, recommended: str) -> List[str]:
145
+ """取得反模式警告"""
146
+ query_lower = query.lower()
147
+ warnings = []
148
+
149
+ if recommended in ANTI_PATTERNS:
150
+ for keyword, warning in ANTI_PATTERNS[recommended]:
151
+ if keyword.lower() in query_lower:
152
+ warnings.append(warning)
153
+
154
+ return warnings
155
+
156
+
157
+ def recommend(query: str, verbose: bool = False) -> Dict[str, Any]:
158
+ """
159
+ 推薦加值中心
160
+
161
+ Args:
162
+ query: 使用者需求描述
163
+ verbose: 是否輸出詳細資訊
164
+
165
+ Returns:
166
+ 推薦結果
167
+ """
168
+ providers = load_providers()
169
+ scores = analyze_requirements(query)
170
+
171
+ # 排序取得推薦順序
172
+ sorted_providers = sorted(
173
+ scores.items(),
174
+ key=lambda x: x[1][0],
175
+ reverse=True
176
+ )
177
+
178
+ # 建立結果
179
+ recommended = sorted_providers[0][0]
180
+ recommended_score, recommended_reasons = sorted_providers[0]
181
+
182
+ # 如果沒有匹配任何關鍵字,給預設推薦
183
+ if recommended_score == 0:
184
+ recommended = 'ECPay'
185
+ recommended_reasons = ['市佔率最高,適合大多數場景', '文檔完整,社群支援豐富']
186
+ recommended_score = 1
187
+
188
+ # 取得加值中心詳細資訊
189
+ provider_info = None
190
+ for p in providers:
191
+ if p.get('provider') == recommended:
192
+ provider_info = p
193
+ break
194
+
195
+ # 取得警告
196
+ warnings = get_anti_pattern_warnings(query, recommended)
197
+
198
+ result = {
199
+ 'query': query,
200
+ 'recommended': recommended,
201
+ 'score': recommended_score,
202
+ 'reasons': recommended_reasons,
203
+ 'warnings': warnings,
204
+ 'alternatives': [],
205
+ 'provider_info': provider_info,
206
+ }
207
+
208
+ # 加入替代方案
209
+ for provider, (score, reasons) in sorted_providers[1:]:
210
+ if score > 0:
211
+ result['alternatives'].append({
212
+ 'provider': provider,
213
+ 'score': score,
214
+ 'reasons': reasons,
215
+ })
216
+
217
+ return result
218
+
219
+
220
+ def format_ascii_box(result: Dict[str, Any]) -> str:
221
+ """格式化為 ASCII Box 輸出"""
222
+ width = 70
223
+ lines = []
224
+
225
+ # 頂部邊框
226
+ lines.append('╔' + '═' * (width - 2) + '╗')
227
+ lines.append('║' + ' 🎯 加值中心推薦結果 '.center(width - 2) + '║')
228
+ lines.append('╠' + '═' * (width - 2) + '╣')
229
+
230
+ # 查詢內容
231
+ query_line = f' 需求: {result["query"]}'
232
+ if len(query_line) > width - 4:
233
+ query_line = query_line[:width - 7] + '...'
234
+ lines.append('║' + query_line.ljust(width - 2) + '║')
235
+ lines.append('╠' + '─' * (width - 2) + '╣')
236
+
237
+ # 推薦結果
238
+ recommended = result['recommended']
239
+ score = result['score']
240
+ lines.append('║' + f' ⭐ 推薦: {recommended} (信心分數: {score})'.ljust(width - 2) + '║')
241
+ lines.append('║' + ' '.ljust(width - 2) + '║')
242
+
243
+ # 推薦原因
244
+ lines.append('║' + ' 📋 推薦原因:'.ljust(width - 2) + '║')
245
+ for reason in result['reasons']:
246
+ reason_line = f' • {reason}'
247
+ if len(reason_line) > width - 4:
248
+ reason_line = reason_line[:width - 7] + '...'
249
+ lines.append('║' + reason_line.ljust(width - 2) + '║')
250
+
251
+ # 警告
252
+ if result['warnings']:
253
+ lines.append('║' + ' '.ljust(width - 2) + '║')
254
+ lines.append('║' + ' ⚠️ 注意事項:'.ljust(width - 2) + '║')
255
+ for warning in result['warnings']:
256
+ warning_line = f' • {warning}'
257
+ if len(warning_line) > width - 4:
258
+ warning_line = warning_line[:width - 7] + '...'
259
+ lines.append('║' + warning_line.ljust(width - 2) + '║')
260
+
261
+ # 替代方案
262
+ if result['alternatives']:
263
+ lines.append('║' + ' '.ljust(width - 2) + '║')
264
+ lines.append('╠' + '─' * (width - 2) + '╣')
265
+ lines.append('║' + ' 🔄 替代方案:'.ljust(width - 2) + '║')
266
+ for alt in result['alternatives']:
267
+ alt_line = f' • {alt["provider"]} (分數: {alt["score"]})'
268
+ lines.append('║' + alt_line.ljust(width - 2) + '║')
269
+ for reason in alt['reasons'][:2]: # 只顯示前 2 個原因
270
+ reason_line = f' - {reason}'
271
+ if len(reason_line) > width - 4:
272
+ reason_line = reason_line[:width - 7] + '...'
273
+ lines.append('║' + reason_line.ljust(width - 2) + '║')
274
+
275
+ # 加值中心資訊
276
+ if result['provider_info']:
277
+ info = result['provider_info']
278
+ lines.append('║' + ' '.ljust(width - 2) + '║')
279
+ lines.append('╠' + '─' * (width - 2) + '╣')
280
+ lines.append('║' + f' 📦 {info.get("display_name", recommended)} 資訊:'.ljust(width - 2) + '║')
281
+ lines.append('║' + f' 認證方式: {info.get("auth_method", "N/A")}'.ljust(width - 2) + '║')
282
+ lines.append('║' + f' 測試網址: {info.get("test_url", "N/A")}'.ljust(width - 2) + '║')
283
+ features = info.get('features', '')
284
+ if features:
285
+ feat_line = f' 特色: {features}'
286
+ if len(feat_line) > width - 4:
287
+ feat_line = feat_line[:width - 7] + '...'
288
+ lines.append('║' + feat_line.ljust(width - 2) + '║')
289
+
290
+ # 底部邊框
291
+ lines.append('╚' + '═' * (width - 2) + '╝')
292
+
293
+ return '\n'.join(lines)
294
+
295
+
296
+ def format_json(result: Dict[str, Any]) -> str:
297
+ """格式化為 JSON 輸出"""
298
+ import json
299
+ return json.dumps(result, ensure_ascii=False, indent=2)
300
+
301
+
302
+ def format_simple(result: Dict[str, Any]) -> str:
303
+ """格式化為簡單文字輸出"""
304
+ lines = []
305
+ lines.append(f"推薦加值中心: {result['recommended']}")
306
+ lines.append(f"信心分數: {result['score']}")
307
+ lines.append("")
308
+ lines.append("推薦原因:")
309
+ for reason in result['reasons']:
310
+ lines.append(f" - {reason}")
311
+
312
+ if result['warnings']:
313
+ lines.append("")
314
+ lines.append("注意事項:")
315
+ for warning in result['warnings']:
316
+ lines.append(f" - {warning}")
317
+
318
+ if result['alternatives']:
319
+ lines.append("")
320
+ lines.append("替代方案:")
321
+ for alt in result['alternatives']:
322
+ lines.append(f" - {alt['provider']} (分數: {alt['score']})")
323
+
324
+ return '\n'.join(lines)
325
+
326
+
327
+ def main():
328
+ parser = argparse.ArgumentParser(
329
+ description='Taiwan Invoice 加值中心推薦系統',
330
+ formatter_class=argparse.RawDescriptionHelpFormatter,
331
+ epilog="""
332
+ 範例:
333
+ python recommend.py "電商 高交易量 穩定"
334
+ python recommend.py "簡單整合 快速上線" --format json
335
+ python recommend.py "API設計優先 MIG標準" --format simple
336
+
337
+ 關鍵字範例:
338
+ 穩定性: 穩定, 市佔, 高交易量, 電商
339
+ 簡易性: 簡單, 快速, 小型, 測試
340
+ API品質: api, 設計, 新, mig, 標準
341
+ 功能: b2b, b2c, 列印, 作廢, 折讓, 載具, 捐贈
342
+ """
343
+ )
344
+
345
+ parser.add_argument('query', help='需求描述 (關鍵字以空格分隔)')
346
+ parser.add_argument(
347
+ '-f', '--format',
348
+ choices=['ascii', 'json', 'simple'],
349
+ default='ascii',
350
+ help='輸出格式 (預設: ascii)'
351
+ )
352
+ parser.add_argument(
353
+ '-v', '--verbose',
354
+ action='store_true',
355
+ help='顯示詳細資訊'
356
+ )
357
+
358
+ args = parser.parse_args()
359
+
360
+ # 執行推薦
361
+ result = recommend(args.query, args.verbose)
362
+
363
+ # 輸出結果
364
+ if args.format == 'json':
365
+ print(format_json(result))
366
+ elif args.format == 'simple':
367
+ print(format_simple(result))
368
+ else:
369
+ print(format_ascii_box(result))
370
+
371
+
372
+ if __name__ == '__main__':
373
+ main()