taiwan-invoice-skill 2.2.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 +34 -1
- package/assets/taiwan-invoice/data/reasoning.csv +32 -0
- package/assets/taiwan-invoice/scripts/core.py +7 -1
- package/assets/taiwan-invoice/scripts/persist.py +330 -0
- package/assets/taiwan-invoice/scripts/recommend.py +34 -1
- package/assets/taiwan-invoice/scripts/search.py +80 -8
- package/dist/index.js +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -119,6 +119,23 @@ taiwan-invoice init --global # 安裝到全域目錄
|
|
|
119
119
|
|
|
120
120
|
---
|
|
121
121
|
|
|
122
|
+
## 智能工具
|
|
123
|
+
|
|
124
|
+
安裝後包含以下 Python 工具(純 Python,無需外部依賴):
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# BM25 搜索引擎 - 搜索錯誤碼、欄位映射、稅務規則
|
|
128
|
+
python scripts/search.py "10000016" --domain error
|
|
129
|
+
|
|
130
|
+
# 加值中心推薦系統 - 根據需求推薦服務商
|
|
131
|
+
python scripts/recommend.py "電商 高交易量 穩定"
|
|
132
|
+
|
|
133
|
+
# 代碼生成器 - 生成 TypeScript/Python 服務模組
|
|
134
|
+
python scripts/generate-invoice-service.py ECPay --output ts
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
122
139
|
## 授權
|
|
123
140
|
|
|
124
141
|
[MIT License](https://github.com/Moksa1123/taiwan-invoice/blob/main/LICENSE)
|
|
@@ -21,7 +21,8 @@ user-invocable: true
|
|
|
21
21
|
- `scripts/search.py` - BM25 搜索引擎(查詢 API、錯誤碼、欄位映射)
|
|
22
22
|
- `scripts/recommend.py` - 加值中心推薦系統
|
|
23
23
|
- `scripts/generate-invoice-service.py` - 服務代碼生成器
|
|
24
|
-
- `
|
|
24
|
+
- `scripts/persist.py` - 持久化配置工具(MASTER.md 生成)
|
|
25
|
+
- `data/` - CSV 數據檔(providers, operations, error-codes, field-mappings, tax-rules, troubleshooting, reasoning)
|
|
25
26
|
|
|
26
27
|
### 何時使用此技能
|
|
27
28
|
- 開發電子發票開立功能
|
|
@@ -66,6 +67,7 @@ python scripts/search.py "折讓" --format json
|
|
|
66
67
|
| `field` | 欄位映射 | field-mappings.csv |
|
|
67
68
|
| `tax` | 稅務計算規則 | tax-rules.csv |
|
|
68
69
|
| `troubleshoot` | 疑難排解 | troubleshooting.csv |
|
|
70
|
+
| `reasoning` | 推薦決策規則 | reasoning.csv |
|
|
69
71
|
|
|
70
72
|
### 推薦系統 (recommend.py)
|
|
71
73
|
|
|
@@ -108,6 +110,37 @@ python scripts/generate-invoice-service.py SmilePay --output py
|
|
|
108
110
|
python scripts/generate-invoice-service.py Amego --output ts > amego-service.ts
|
|
109
111
|
```
|
|
110
112
|
|
|
113
|
+
### 持久化配置 (persist.py)
|
|
114
|
+
|
|
115
|
+
將發票配置保存為 MASTER.md,供 AI 助手持續參考:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# 初始化配置
|
|
119
|
+
python scripts/persist.py init ECPay
|
|
120
|
+
python scripts/persist.py init SmilePay -p "MyProject"
|
|
121
|
+
|
|
122
|
+
# 顯示當前配置
|
|
123
|
+
python scripts/persist.py show
|
|
124
|
+
|
|
125
|
+
# 列出可用服務商
|
|
126
|
+
python scripts/persist.py list
|
|
127
|
+
|
|
128
|
+
# 強制覆蓋
|
|
129
|
+
python scripts/persist.py init Amego --force
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**生成結構:**
|
|
133
|
+
```
|
|
134
|
+
invoice-config/
|
|
135
|
+
└── MASTER.md # 專案發票配置
|
|
136
|
+
├── 基本資訊
|
|
137
|
+
├── 服務商配置
|
|
138
|
+
├── API 端點
|
|
139
|
+
├── 發票類型設定
|
|
140
|
+
├── 環境變數建議
|
|
141
|
+
└── 開發檢查清單
|
|
142
|
+
```
|
|
143
|
+
|
|
111
144
|
---
|
|
112
145
|
|
|
113
146
|
## 發票類型
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
scenario,recommended_provider,confidence,reason,decision_rules,anti_patterns,use_cases
|
|
2
|
+
高交易量電商,ECPay,HIGH,市佔率最高 系統穩定性佳,volume>1000/day AND priority=stability,避免使用測試環境長期運行,大型電商平台 購物網站
|
|
3
|
+
穩定性優先,ECPay,HIGH,台灣電子發票市佔率領先,priority=stability OR risk_tolerance=low,避免頻繁切換服務商,金融相關 醫療系統
|
|
4
|
+
完整文檔需求,ECPay,HIGH,提供完整 API 文檔與 SDK,priority=documentation OR team_size>3,避免依賴社群資源,企業級開發 團隊協作
|
|
5
|
+
多語言SDK需求,ECPay,MEDIUM,官方SDK支援多種語言,language IN (java php dotnet) AND need_sdk=true,避免自行實作加密,Java/.NET專案
|
|
6
|
+
簡單整合需求,SmilePay,HIGH,整合流程最簡單 無需複雜加密,priority=speed OR deadline<7days,避免過度設計,MVP開發 快速上線
|
|
7
|
+
小型專案,SmilePay,HIGH,適合小型專案和個人開發者,team_size<=2 AND budget=low,避免過度工程化,個人專案 小型網站
|
|
8
|
+
快速上線,SmilePay,HIGH,最快速完成整合,deadline<14days AND complexity=low,避免完美主義,緊急專案 POC驗證
|
|
9
|
+
測試環境友善,SmilePay,MEDIUM,測試環境設定簡單,phase=development OR phase=testing,避免正式環境測試,開發階段 測試階段
|
|
10
|
+
低預算專案,SmilePay,MEDIUM,費用相對較低,budget=low AND volume<500/day,避免過度投資,新創公司 個人開發
|
|
11
|
+
API設計優先,Amego,HIGH,MIG 4.0 最新 API 標準,priority=api_design OR tech_stack=modern,避免使用舊版API,技術導向團隊
|
|
12
|
+
現代技術棧,Amego,HIGH,採用最新技術標準,framework IN (nextjs nuxt sveltekit),避免傳統架構,現代前端專案
|
|
13
|
+
RESTful需求,Amego,MEDIUM,API設計符合REST規範,priority=clean_code AND experience>3years,避免過度抽象,資深開發團隊
|
|
14
|
+
MIG標準合規,Amego,HIGH,完整支援MIG 4.0規範,compliance=mig40 OR government=true,避免自定義協議,政府專案 標準合規
|
|
15
|
+
B2B發票為主,ECPay,MEDIUM,B2B發票功能完整 經驗豐富,invoice_type=b2b AND volume>100/day,避免B2C邏輯套用B2B,企業對企業交易
|
|
16
|
+
B2C發票為主,ECPay,MEDIUM,B2C市佔最高 問題解決資源多,invoice_type=b2c AND support_need=high,避免忽視客服需求,一般消費者交易
|
|
17
|
+
B2C簡單整合,SmilePay,MEDIUM,B2C整合最簡單,invoice_type=b2c AND priority=speed,避免過度複雜化,簡單B2C場景
|
|
18
|
+
混合發票需求,ECPay,MEDIUM,同時支援B2B/B2C 切換彈性,invoice_type=mixed,避免硬編碼發票類型,B2B+B2C混合
|
|
19
|
+
載具功能需求,ECPay,MEDIUM,載具支援最完整,feature=carrier AND carrier_type IN (mobile barcode),避免忽略載具驗證,手機條碼整合
|
|
20
|
+
捐贈功能需求,ECPay,MEDIUM,捐贈功能完整,feature=donation,避免捐贈與載具混用,公益捐贈整合
|
|
21
|
+
列印功能需求,ECPay,MEDIUM,列印功能完整 多種格式,feature=print AND print_format IN (a4 thermal),避免忽略隨機碼,實體發票列印
|
|
22
|
+
作廢功能需求,ECPay,LOW,作廢流程標準化,feature=void,避免作廢後重開相同號碼,發票作廢處理
|
|
23
|
+
折讓功能需求,ECPay,LOW,折讓功能完整,feature=allowance,避免折讓超過原發票金額,退款折讓處理
|
|
24
|
+
無技術資源,SmilePay,HIGH,無需複雜加密 整合門檻最低,tech_resource=limited OR developer_count=1,避免選擇高複雜度方案,一人團隊 非技術背景
|
|
25
|
+
AES加密經驗,ECPay,MEDIUM,需要AES-128-CBC加密經驗,encryption_experience=aes,避免明文傳輸,有加密經驗團隊
|
|
26
|
+
MD5簽章經驗,Amego,MEDIUM,使用MD5簽章驗證,encryption_experience=md5,避免簽章順序錯誤,有MD5經驗團隊
|
|
27
|
+
Node.js專案,ECPay,MEDIUM,官方有Node.js範例,language=nodejs AND need_example=true,避免從零實作加密,Node.js開發
|
|
28
|
+
Python專案,ECPay,MEDIUM,社群Python範例豐富,language=python,避免依賴過時範例,Python開發
|
|
29
|
+
PHP專案,ECPay,HIGH,官方PHP SDK最完整,language=php AND need_sdk=true,避免版本不相容,PHP開發
|
|
30
|
+
新創公司,SmilePay,MEDIUM,快速上線 成本控制,company_type=startup AND phase=early,避免過度投資基礎建設,早期新創
|
|
31
|
+
企業級應用,ECPay,HIGH,穩定性和支援優先,company_type=enterprise,避免使用非主流方案,大型企業
|
|
32
|
+
政府專案,Amego,MEDIUM,MIG標準合規,project_type=government,避免非標準協議,政府標案
|
|
@@ -47,6 +47,11 @@ CSV_CONFIG = {
|
|
|
47
47
|
'file': 'troubleshooting.csv',
|
|
48
48
|
'search_cols': ['issue', 'symptom', 'cause', 'solution', 'provider', 'category'],
|
|
49
49
|
'output_cols': ['issue', 'symptom', 'cause', 'solution', 'provider', 'severity']
|
|
50
|
+
},
|
|
51
|
+
'reasoning': {
|
|
52
|
+
'file': 'reasoning.csv',
|
|
53
|
+
'search_cols': ['scenario', 'recommended_provider', 'reason', 'decision_rules', 'use_cases'],
|
|
54
|
+
'output_cols': ['scenario', 'recommended_provider', 'confidence', 'reason', 'anti_patterns', 'use_cases']
|
|
50
55
|
}
|
|
51
56
|
}
|
|
52
57
|
|
|
@@ -57,7 +62,8 @@ DOMAIN_KEYWORDS = {
|
|
|
57
62
|
'error': ['error', 'code', '錯誤', '代碼', '失敗', 'fail', '-', '10000', '1001', '2001'],
|
|
58
63
|
'field': ['field', 'param', '欄位', '參數', 'mapping', '映射', 'merchantid', 'orderid', 'buyername'],
|
|
59
64
|
'tax': ['tax', 'b2c', 'b2b', '稅', '應稅', '免稅', '零稅率', 'salesamount', 'taxamount', '計算'],
|
|
60
|
-
'troubleshoot': ['問題', 'issue', 'error', 'fix', '解決', '失敗', '空白', 'troubleshoot', '踩坑']
|
|
65
|
+
'troubleshoot': ['問題', 'issue', 'error', 'fix', '解決', '失敗', '空白', 'troubleshoot', '踩坑'],
|
|
66
|
+
'reasoning': ['推薦', 'recommend', '選擇', 'choose', '適合', 'suitable', '場景', 'scenario', '決策', 'decision']
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
|
|
@@ -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()
|
|
@@ -82,6 +82,17 @@ def load_providers() -> List[Dict[str, str]]:
|
|
|
82
82
|
return list(reader)
|
|
83
83
|
|
|
84
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
|
+
|
|
85
96
|
def analyze_requirements(query: str) -> Dict[str, Tuple[int, List[str]]]:
|
|
86
97
|
"""
|
|
87
98
|
分析使用者需求,計算各加值中心分數
|
|
@@ -97,7 +108,29 @@ def analyze_requirements(query: str) -> Dict[str, Tuple[int, List[str]]]:
|
|
|
97
108
|
'Amego': (0, []),
|
|
98
109
|
}
|
|
99
110
|
|
|
100
|
-
#
|
|
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)
|
|
101
134
|
for keyword, rules in RECOMMENDATION_RULES.items():
|
|
102
135
|
if keyword.lower() in query_lower:
|
|
103
136
|
for provider, weight, reason in rules:
|
|
@@ -23,22 +23,29 @@ from core import (
|
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def format_ascii_box(title: str, content: List[str], width: int = 80) -> str:
|
|
26
|
+
def format_ascii_box(title: str, content: List[str], width: int = 80, style: str = 'double') -> str:
|
|
27
27
|
"""
|
|
28
28
|
格式化 ASCII Box 輸出
|
|
29
|
+
|
|
30
|
+
style: 'single' (─│) or 'double' (═║)
|
|
29
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
|
+
|
|
30
37
|
lines = []
|
|
31
|
-
lines.append(
|
|
32
|
-
lines.append(
|
|
33
|
-
lines.append(
|
|
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)
|
|
34
41
|
|
|
35
42
|
for line in content:
|
|
36
43
|
# 處理過長的行
|
|
37
44
|
if len(line) > width - 4:
|
|
38
45
|
line = line[:width - 7] + '...'
|
|
39
|
-
lines.append('
|
|
46
|
+
lines.append(v + ' ' + line.ljust(width - 4) + ' ' + v)
|
|
40
47
|
|
|
41
|
-
lines.append(
|
|
48
|
+
lines.append(bl + h * (width - 2) + br)
|
|
42
49
|
return '\n'.join(lines)
|
|
43
50
|
|
|
44
51
|
|
|
@@ -104,6 +111,66 @@ def format_all_results(results: Dict[str, List[Dict[str, Any]]], query: str) ->
|
|
|
104
111
|
return '\n'.join(output)
|
|
105
112
|
|
|
106
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
|
+
|
|
107
174
|
def list_domains():
|
|
108
175
|
"""
|
|
109
176
|
列出所有可用的搜索域
|
|
@@ -148,7 +215,7 @@ Examples:
|
|
|
148
215
|
help='Search all domains')
|
|
149
216
|
parser.add_argument('-l', '--list', action='store_true',
|
|
150
217
|
help='List available domains')
|
|
151
|
-
parser.add_argument('-f', '--format', choices=['ascii', 'simple', 'json'],
|
|
218
|
+
parser.add_argument('-f', '--format', choices=['ascii', 'simple', 'json', 'markdown', 'md'],
|
|
152
219
|
default='ascii', help='Output format (default: ascii)')
|
|
153
220
|
|
|
154
221
|
args = parser.parse_args()
|
|
@@ -172,6 +239,8 @@ Examples:
|
|
|
172
239
|
if args.format == 'json':
|
|
173
240
|
import json
|
|
174
241
|
print(json.dumps(results, ensure_ascii=False, indent=2))
|
|
242
|
+
elif args.format in ('markdown', 'md'):
|
|
243
|
+
print(format_markdown_all(results, query))
|
|
175
244
|
else:
|
|
176
245
|
print(format_all_results(results, query))
|
|
177
246
|
return
|
|
@@ -180,13 +249,16 @@ Examples:
|
|
|
180
249
|
domain = args.domain
|
|
181
250
|
if not domain:
|
|
182
251
|
domain = detect_domain(query)
|
|
183
|
-
|
|
252
|
+
if args.format not in ('json', 'markdown', 'md'):
|
|
253
|
+
print(f"[Auto-detected domain: {domain}]")
|
|
184
254
|
|
|
185
255
|
results = search(query, domain, args.max_results)
|
|
186
256
|
|
|
187
257
|
if args.format == 'json':
|
|
188
258
|
import json
|
|
189
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))
|
|
190
262
|
elif args.format == 'simple':
|
|
191
263
|
for i, result in enumerate(results, 1):
|
|
192
264
|
print(f"\n[{i}] Score: {result.get('_score', 0)}")
|
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);
|