-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathatom.xml
More file actions
736 lines (357 loc) · 211 KB
/
atom.xml
File metadata and controls
736 lines (357 loc) · 211 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>亂馬客</title>
<icon>https://www.gravatar.com/avatar/cd3aed042ccd7a5a5d9956b0bc07dc81</icon>
<subtitle>Stay Hungry, Stay Foolish.</subtitle>
<link href="https://rainmakerho.github.io/atom.xml" rel="self"/>
<link href="https://rainmakerho.github.io/"/>
<updated>2026-01-23T09:33:54.525Z</updated>
<id>https://rainmakerho.github.io/</id>
<author>
<name>亂馬客</name>
<email>rainmaker_ho@gss.com.tw</email>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>影像 OCR 實戰心得:LLM 與 Azure Document Intelligence 的選擇策略</title>
<link href="https://rainmakerho.github.io/2026/01/23/ocr-llm-vs-azure-document-intelligence-experience/"/>
<id>https://rainmakerho.github.io/2026/01/23/ocr-llm-vs-azure-document-intelligence-experience/</id>
<published>2026-01-23T09:00:32.000Z</published>
<updated>2026-01-23T09:33:54.525Z</updated>
<content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>在實務上使用大型語言模型(LLM)進行影像內容解析(OCR + 內容理解)時,<br>並不存在一個模型可以適用所有情境的解法。</p><p>影像的<strong>數量、解析度、內容多寡,以及是否包含表格或手寫文字</strong>,<br>都會大幅影響解析結果的正確性與穩定度。</p><p>本文整理實際專案中,針對 <strong>LLM(GPT-4.1-mini、Gemini)</strong><br>以及 <strong>Azure Document Intelligence</strong> 在不同影像解析場景下的使用心得。</p><hr><h2 id="情境一:單張或少量影像、內容單純(無表格)"><a href="#情境一:單張或少量影像、內容單純(無表格)" class="headerlink" title="情境一:單張或少量影像、內容單純(無表格)"></a>情境一:單張或少量影像、內容單純(無表格)</h2><h3 id="特性"><a href="#特性" class="headerlink" title="特性"></a>特性</h3><ul><li>單張或少量影像</li><li>內容不多</li><li>無表格結構</li><li>以印刷體文字為主</li></ul><h3 id="建議工具"><a href="#建議工具" class="headerlink" title="建議工具"></a>建議工具</h3><ul><li><strong>GPT-4.1-mini/Gemini 3 Flash</strong></li></ul><h3 id="實際案例"><a href="#實際案例" class="headerlink" title="實際案例"></a>實際案例</h3><ul><li>加油發票</li><li>簡單收據</li><li>欄位固定、格式單純的文件</li></ul><p>在這類情境中,直接使用 LLM 解析影像即可,<br>具備速度快、成本低且彈性高的優勢。</p><hr><h2 id="情境二:多張影像同時辨識(解析度影響明顯)"><a href="#情境二:多張影像同時辨識(解析度影響明顯)" class="headerlink" title="情境二:多張影像同時辨識(解析度影響明顯)"></a>情境二:多張影像同時辨識(解析度影響明顯)</h2><h3 id="特性-1"><a href="#特性-1" class="headerlink" title="特性"></a>特性</h3><ul><li>一次輸入多張影像</li><li>例如:一頁中包含 5 張加油發票</li><li>單張影像解析度偏低</li></ul><h3 id="實務觀察"><a href="#實務觀察" class="headerlink" title="實務觀察"></a>實務觀察</h3><ul><li><p><strong>GPT-4.1-mini</strong></p><ul><li>當影像解析度不足時,中文辨識可能出現錯字或漏字</li></ul></li><li><p><strong>Gemini 3 Flash</strong></p><ul><li>在相同條件下,能完整且正確擷取中文內容</li></ul></li><li><p><strong>Azure Document Intelligence</strong></p><ul><li>因缺乏表格結構,多張發票內容容易發生欄位或文字錯置</li></ul></li><li><p>註:在「多張影像、低解析度、無表格」的情境下,<br><strong>Gemini 3 Flash 的穩定度與中文辨識表現較佳</strong>。</p></li></ul><hr><h2 id="情境三:影像包含表格、內容多或有手寫文字"><a href="#情境三:影像包含表格、內容多或有手寫文字" class="headerlink" title="情境三:影像包含表格、內容多或有手寫文字"></a>情境三:影像包含表格、內容多或有手寫文字</h2><h3 id="特性-2"><a href="#特性-2" class="headerlink" title="特性"></a>特性</h3><ul><li>具備明確表格結構</li><li>內容量大</li><li>同時包含印刷體與手寫文字</li></ul><h3 id="建議工具-1"><a href="#建議工具-1" class="headerlink" title="建議工具"></a>建議工具</h3><ul><li><strong>Azure Document Intelligence</strong></li></ul><h3 id="實際案例-1"><a href="#實際案例-1" class="headerlink" title="實際案例"></a>實際案例</h3><ul><li>電費單</li><li>水費單</li><li>帳單、報表類文件</li></ul><p>這類文件結構明確,<br>Azure Document Intelligence 在<strong>表格解析、欄位對齊與手寫文字辨識</strong>方面表現穩定,<br>比純 LLM 更適合長篇且結構化的文件。</p><hr><h2 id="整體選擇建議總結"><a href="#整體選擇建議總結" class="headerlink" title="整體選擇建議總結"></a>整體選擇建議總結</h2><p>可簡單歸納為以下原則:</p><ul><li><strong>有表格、內容多、包含手寫文字</strong><ul><li>優先使用 <strong>Azure Document Intelligence</strong></li></ul></li><li><strong>內容少、無表格</strong><ul><li>使用 <strong>LLM(GPT-4.1-mini 或 Gemini)</strong></li></ul></li><li><strong>多張影像、解析度偏低</strong><ul><li>優先考慮 <strong>Gemini 3 Flash</strong></li></ul></li></ul><p>但使用 <strong>Azure Document Intelligence</strong> 後,通常會再把內容給 LLM 來整理出需要的內容。<br>如果只能在地端的話,以<strong>中國</strong>的模型效果比較好,例如 Qwen 的模型。<br>如果是特別的領域,可以拿<strong>中國</strong>的模型再來 Finetune 成自家需要的 Model。</p><hr><h2 id="結語"><a href="#結語" class="headerlink" title="結語"></a>結語</h2><p>影像 OCR 並不存在「一個工具打天下」的最佳解法,<br><strong>理解文件特性並選擇合適的工具</strong>,往往比追求最新模型更重要。</p><p>在實務系統中,混合使用 <strong>LLM 與文件解析服務</strong>,<br>通常能在成本、準確率與穩定度之間取得更好的平衡。</p><p>希望這些實戰心得,能對正在進行影像解析或文件自動化的你有所幫助。</p>]]></content>
<summary type="html"><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>在實務上使用大型語言模型(LLM)進行影像內容解析(OCR + 內容理解)時,<br>並不存在一個模型可以適用所有情境的解法。</p>
<p</summary>
<category term="azure document intelligence" scheme="https://rainmakerho.github.io/tags/azure-document-intelligence/"/>
<category term="gpt" scheme="https://rainmakerho.github.io/tags/gpt/"/>
<category term="gemini" scheme="https://rainmakerho.github.io/tags/gemini/"/>
<category term="ocr" scheme="https://rainmakerho.github.io/tags/ocr/"/>
</entry>
<entry>
<title>推動「綠色軟體」的同時,我們是否正陷入地端 AI 的排碳陷阱?</title>
<link href="https://rainmakerho.github.io/2025/12/25/tw-green-software/"/>
<id>https://rainmakerho.github.io/2025/12/25/tw-green-software/</id>
<published>2025-12-25T08:07:40.000Z</published>
<updated>2025-12-25T08:43:15.273Z</updated>
<content type="html"><![CDATA[<h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>最近資訊業界最熱門的話題莫過於「綠色軟體(Green Software)」,其核心目標在於優化資訊架構,達成節能減碳。<br>但是隨著 AI 的推動,卻發現一個巨大的矛盾。</p><blockquote><p>政府與企業一邊高喊節能減碳,另一邊各單位卻為了資安焦慮,紛紛編列預算各自採購地端 GPU 伺服器</p></blockquote><p>這種各地的 GPU 伺服器,真的符合綠色永續嗎?</p><h4 id="地端-AI-的真實困境:低效能與高能耗的雙重夾擊"><a href="#地端-AI-的真實困境:低效能與高能耗的雙重夾擊" class="headerlink" title="地端 AI 的真實困境:低效能與高能耗的雙重夾擊"></a>地端 AI 的真實困境:低效能與高能耗的雙重夾擊</h4><p>地端的 Model 跟 雲端 AI 模型(如 GPT-5, Gemini)相比,地端 AI 的理解力與準確率還是遜於雲端,最後或許還要耗費大量人工校對。</p><p>AI 技術快速更新。雲端服務能即時導入最新演算法,但地端系統從採購到建置完成可能已過時,會不會陷入「花大錢買舊技術」的循環之中呢。<br>一台 GPU Server 隨便都是上百萬台幣,這筆經費若轉為雲端算力,足以供應單位使用數年且隨時保持在最強效能。</p><p>自購伺服器多半僅在特定專案或上班時間運作,但為了維持機房恆溫與待機,閒置時依然消耗大量電力。<br>比起雲端資料中心極致的能源使用效率,地端機房才是真正的排碳大戶。</p><h3 id="資料分級:打破「一刀切」的資安迷思"><a href="#資料分級:打破「一刀切」的資安迷思" class="headerlink" title="資料分級:打破「一刀切」的資安迷思"></a>資料分級:打破「一刀切」的資安迷思</h3><p>整個單位中,並不是所有的資料都是機密,透過資料的分級,可以讓 AI 效能最大化。<br>一些非機敏資料使用雲端 AI 來改善單位流程及效率,而不是一刀切,完全不能使用雲端 AI。</p><h3 id="結論"><a href="#結論" class="headerlink" title="結論"></a>結論</h3><p>「綠色軟體」政策不應僅止於程式碼的優化,更應包含佈署架構的智慧化。<br>我們必須承認:分散式的 GPU 伺服器往往是能源效率的殺手。</p><h3 id="參考來源"><a href="#參考來源" class="headerlink" title="參考來源"></a>參考來源</h3><p><a href="https://www.teema.org.tw/industry-information-detail.aspx?infoid=50589">從程式碼到雲端:實踐數位永續的綠色軟體全攻略</a></p>]]></content>
<summary type="html"><h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>最近資訊業界最熱門的話題莫過於「綠色軟體(Green Software)」,其核心目標在於優化資訊架構,達成節能減碳。<br>但是隨著 AI</summary>
<category term="綠色軟體" scheme="https://rainmakerho.github.io/tags/%E7%B6%A0%E8%89%B2%E8%BB%9F%E9%AB%94/"/>
<category term="GPU" scheme="https://rainmakerho.github.io/tags/GPU/"/>
</entry>
<entry>
<title>.NET 6 System.Data.SqlClient 查看 Connection 效能計數器</title>
<link href="https://rainmakerho.github.io/2025/12/22/net6-system-data-sqlclient-perfmon-connection-pool/"/>
<id>https://rainmakerho.github.io/2025/12/22/net6-system-data-sqlclient-perfmon-connection-pool/</id>
<published>2025-12-22T02:55:04.000Z</published>
<updated>2025-12-22T03:38:49.437Z</updated>
<content type="html"><![CDATA[<h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>.NET 6 透過 System.Data.SqlClient 連接資料庫,想要在 Performance Monitor 中加入 <strong>.NET Data Provider for SqlServer</strong> 來查看 Connection Pool 的相關資料,但是在 <strong>Instances of selected object:</strong> 中,卻找不到對應的 Process Id</p><p>在 .NET 6 可以透過 <strong>dotnet-counters</strong> 來查看<strong>Microsoft.Data.SqlClient</strong>,而不是<strong>System.Data.SqlClient</strong></p><h3 id="解法"><a href="#解法" class="headerlink" title="解法"></a>解法</h3><p>Performance Monitor 工具是查看 .NET Framework 的程式,.NET 6 要使用 <strong>dotnet-counters</strong> 。<br>但是<strong>dotnet-counters</strong>可以看的是<strong>Microsoft.Data.SqlClient</strong>的 EventCounters。</p><h5 id="1-安裝-Microsoft-Data-SqlClient-套件-5-2-3"><a href="#1-安裝-Microsoft-Data-SqlClient-套件-5-2-3" class="headerlink" title="1.安裝 Microsoft.Data.SqlClient 套件(5.2.3)"></a>1.安裝 Microsoft.Data.SqlClient 套件(5.2.3)</h5><h5 id="2-調整-System-Data-SqlClient-Namespace"><a href="#2-調整-System-Data-SqlClient-Namespace" class="headerlink" title="2.調整 System.Data.SqlClient Namespace"></a>2.調整 System.Data.SqlClient Namespace</h5><p>將 System.Data.SqlClient Namespace 改成 Microsoft.Data.SqlClient Namespace</p><h5 id="3-重新建置"><a href="#3-重新建置" class="headerlink" title="3.重新建置"></a>3.重新建置</h5><h3 id="dotnet-counters-monitor"><a href="#dotnet-counters-monitor" class="headerlink" title="dotnet-counters monitor"></a>dotnet-counters monitor</h3><p>1.安裝<strong>dotnet-counters</strong><br><code>dotnet tool install --global dotnet-counters --version 6.0.327302</code></p><p>2.查看可以 monitor 的 process<br><code>dotnet-counters ps</code></p><ul><li>請確定程式已經在執行</li></ul><p>3.監看 Microsoft.Data.SqlClient.EventSource<br><code>dotnet-counters monitor --counters Microsoft.Data.SqlClient.EventSource -p <process-id> --refresh-interval 3</code></p><p>也可以將資料存成 CSV /JSON 格式,例如,<br><code>dotnet-counters collect --counters Microsoft.Data.SqlClient.EventSource --process-id <process-id> --refresh-interval 3 --format csv</code></p><h3 id="參考資源"><a href="#參考資源" class="headerlink" title="參考資源"></a>參考資源</h3><p><a href="https://rainmakerho.github.io/2022/08/29/timeout-expired-max-pool-size-reached/">Timeout expired. all pooled connections were in use and max pool size was reached. 自動關閉 Connection ?</a><br><a href="https://dotblogs.azurewebsites.net/rainmaker/2017/04/26/143316">已超過連接逾時的設定。在取得集區連接之前超過逾時等待的時間,可能的原因為所有的共用連接已在使用中,並已達共用集區大小的最大值。</a><br><a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-counters">dotnet-counters</a></p>]]></content>
<summary type="html"><h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>.NET 6 透過 System.Data.SqlClient 連接資料庫,想要在 Performance Monitor 中加入 <str</summary>
<category term=".net6" scheme="https://rainmakerho.github.io/tags/net6/"/>
<category term="System.Data.SqlClient" scheme="https://rainmakerho.github.io/tags/System-Data-SqlClient/"/>
<category term="Microsoft.Data.SqlClient" scheme="https://rainmakerho.github.io/tags/Microsoft-Data-SqlClient/"/>
<category term="PerfMon" scheme="https://rainmakerho.github.io/tags/PerfMon/"/>
<category term="Connection Pool" scheme="https://rainmakerho.github.io/tags/Connection-Pool/"/>
<category term="performance-counters" scheme="https://rainmakerho.github.io/tags/performance-counters/"/>
<category term="dotnet-counters" scheme="https://rainmakerho.github.io/tags/dotnet-counters/"/>
</entry>
<entry>
<title>MSB4018 工作發生未預期的失敗 DirectoryNotFoundException</title>
<link href="https://rainmakerho.github.io/2025/12/18/MSB4018-VS-ERROR/"/>
<id>https://rainmakerho.github.io/2025/12/18/MSB4018-VS-ERROR/</id>
<published>2025-12-18T02:15:36.000Z</published>
<updated>2025-12-18T08:05:27.870Z</updated>
<content type="html"><![CDATA[<h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>透過 VS.NET 建置專案時,會發生以下的錯誤,</p><blockquote><p>GenerateStaticWebAssetsPropsFile 工作發生未預期的失敗<br>System.IO.DirectoryNotFoundException: 找不到路徑 …</p></blockquote><img src="/2025/12/18/MSB4018-VS-ERROR/01.png" class="" title="GenerateStaticWebAssetsPropsFile"><p>但是如果透過命令視窗卻可以正常建置。</p><h3 id="解法"><a href="#解法" class="headerlink" title="解法"></a>解法</h3><p>發生這個問題是因為專案的路徑太長,所以解法有以下的方式,</p><h4 id="1-縮短路徑"><a href="#1-縮短路徑" class="headerlink" title="1.縮短路徑:"></a>1.縮短路徑:</h4><p>所以可以依 <a href="https://www.youtube.com/watch?v=GtqQn36onAg">How to fix: The GenerateStaticWebAsssetsPropsFile task failed unexpectedly in Visual Studio</a> 的方式,讓專案所在的整個路徑不要那麼長。</p><h4 id="2-允許長路徑-允許超過-260"><a href="#2-允許長路徑-允許超過-260" class="headerlink" title="2.允許長路徑(允許超過 260)"></a>2.允許長路徑(允許超過 260)</h4><p>使用 PowerShell 修改 (需管理員權限): 開啟 PowerShell 並輸入以下指令:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">New-ItemProperty</span> <span class="literal">-Path</span> <span class="string">"HKLM:\System\CurrentControlSet\Control\FileSystem"</span> <span class="literal">-Name</span> <span class="string">"LongPathsEnabled"</span> <span class="literal">-Value</span> <span class="number">1</span> <span class="literal">-PropertyType</span> DWORD <span class="literal">-Force</span></span><br></pre></td></tr></table></figure><p>完成後,重新開啟方案來建置,應該就可以建置成功了。</p><h3 id="參考資源"><a href="#參考資源" class="headerlink" title="參考資源"></a>參考資源</h3><p><a href="https://www.youtube.com/watch?v=GtqQn36onAg">How to fix: The GenerateStaticWebAsssetsPropsFile task failed unexpectedly in Visual Studio</a></p>]]></content>
<summary type="html"><h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>透過 VS.NET 建置專案時,會發生以下的錯誤,</p>
<blockquote>
<p>GenerateStaticWebAssetsP</summary>
<category term="Visual Studio" scheme="https://rainmakerho.github.io/tags/Visual-Studio/"/>
<category term="DirectoryNotFoundException" scheme="https://rainmakerho.github.io/tags/DirectoryNotFoundException/"/>
<category term="MSB4018" scheme="https://rainmakerho.github.io/tags/MSB4018/"/>
<category term="LongPathsEnabled" scheme="https://rainmakerho.github.io/tags/LongPathsEnabled/"/>
</entry>
<entry>
<title>Playwright 部署到 Azure App Service 發生 Driver not found</title>
<link href="https://rainmakerho.github.io/2025/12/02/playwright-Driver-not-found/"/>
<id>https://rainmakerho.github.io/2025/12/02/playwright-Driver-not-found/</id>
<published>2025-12-02T05:38:33.000Z</published>
<updated>2025-12-02T05:46:27.969Z</updated>
<content type="html"><![CDATA[<h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>最近同事使用 <a href="https://playwright.dev/dotnet/docs/intro">Playwright for .NET</a> 將程式部署到 Azure 後,會發生以下的錯誤,</p><blockquote><p>Driver not found: c:\home\site\wwwroot\.playwright\node\win32_x64\node.exe</p></blockquote><h3 id="解法"><a href="#解法" class="headerlink" title="解法"></a>解法</h3><p>這個錯應該是因為在 Publish 後,部署的程式的包含 <strong>.playwright</strong> 的目錄,在部署到 Azure 時,沒有一併將 <strong>.playwright</strong> 的目錄部署上去。<br>所以將 <strong>.playwright</strong> 的目錄一併部署上去就可以了哦!</p><h3 id="參考資源"><a href="#參考資源" class="headerlink" title="參考資源"></a>參考資源</h3><p><a href="https://stackoverflow.com/questions/79585756/playwright-driver-not-found-error-in-azure-function-azure-portal-flex-consump">Playwright driver not found error in Azure Function (Azure Portal - Flex consumption plan)</a><br><a href="https://www.linkedin.com/pulse/using-playwright-windows-hosted-azure-function-devis-giacopuzzi-bqz5f/">Using Playwright in a Windows-hosted Azure Function</a></p>]]></content>
<summary type="html"><h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>最近同事使用 <a href="https://playwright.dev/dotnet/docs/intro">Playwright f</summary>
<category term=".NET" scheme="https://rainmakerho.github.io/tags/NET/"/>
<category term="Azure" scheme="https://rainmakerho.github.io/tags/Azure/"/>
<category term="Playwright" scheme="https://rainmakerho.github.io/tags/Playwright/"/>
</entry>
<entry>
<title>Windows 11 24H2 安裝 Update 後造成 localhost 連不到 ERR_CONNECTION_RESET or hostname is invalid</title>
<link href="https://rainmakerho.github.io/2025/10/16/iis-express-failing-after-install-2025-10-update-for-windows11-24h2/"/>
<id>https://rainmakerho.github.io/2025/10/16/iis-express-failing-after-install-2025-10-update-for-windows11-24h2/</id>
<published>2025-10-15T16:16:15.000Z</published>
<updated>2025-10-15T16:32:27.414Z</updated>
<content type="html"><![CDATA[<h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>今天同事的 Windows 11 24H2 安裝完 Windows 更新重開機後,從 VS.NET 跑 IIS Express 起來後,<br>會發生 <strong>ERR_CONNECTION_RESET</strong> or <strong>hostname is invalid</strong> 的錯誤</p><h3 id="解法"><a href="#解法" class="headerlink" title="解法"></a>解法</h3><p>可以參考 <a href="https://stackoverflow.com/questions/79790827/localhost-applications-failing-after-installing-2025-10-cumulative-update-for-w">Localhost applications failing after installing “2025-10 Cumulative Update for Windows 11 Version 24H2 for x64-based Systems (KB5066835) (26100.6899)”</a> 解法如下:</p><ol><li>更新到 <strong>Windows 11 25H2</strong></li><li>修改機碼,將 <code>HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\HTTP\</code> 中新增 2 個機碼(<code>EnableHttp2Tls</code>及<code>EnableHttp2Cleartext</code>),並設定<code>DWORD (32-bit) Value</code>值為<code>0</code> (未驗證)</li><li>延後更新,並移除 <code>KB5066835</code>, <code>KB5066131</code>, <code>KB5065789</code></li></ol><ul><li>註: 感謝同事 Henry, Simon & Ryan 的幫忙</li></ul><h3 id="參考資源"><a href="#參考資源" class="headerlink" title="參考資源"></a>參考資源</h3><p><a href="https://stackoverflow.com/questions/79790827/localhost-applications-failing-after-installing-2025-10-cumulative-update-for-w">Localhost applications failing after installing “2025-10 Cumulative Update for Windows 11 Version 24H2 for x64-based Systems (KB5066835) (26100.6899)”</a></p>]]></content>
<summary type="html"><h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>今天同事的 Windows 11 24H2 安裝完 Windows 更新重開機後,從 VS.NET 跑 IIS Express 起來後,<b</summary>
<category term="ERR_CONNECTION_RESET" scheme="https://rainmakerho.github.io/tags/ERR-CONNECTION-RESET/"/>
<category term="400" scheme="https://rainmakerho.github.io/tags/400/"/>
<category term="Windows 11" scheme="https://rainmakerho.github.io/tags/Windows-11/"/>
<category term="24H2" scheme="https://rainmakerho.github.io/tags/24H2/"/>
<category term="hostname is invalid" scheme="https://rainmakerho.github.io/tags/hostname-is-invalid/"/>
<category term="KB5066835" scheme="https://rainmakerho.github.io/tags/KB5066835/"/>
<category term="KB5066131" scheme="https://rainmakerho.github.io/tags/KB5066131/"/>
<category term="KB5065789" scheme="https://rainmakerho.github.io/tags/KB5065789/"/>
</entry>
<entry>
<title>.NET Build task failed unexpectedly. System.IO.DirectoryNotFoundException Could not find a part of the path</title>
<link href="https://rainmakerho.github.io/2025/09/15/dotnet-build-directorynotfoundexception/"/>
<id>https://rainmakerho.github.io/2025/09/15/dotnet-build-directorynotfoundexception/</id>
<published>2025-09-15T05:48:05.000Z</published>
<updated>2025-09-15T06:01:31.852Z</updated>
<content type="html"><![CDATA[<h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>在 .NET Build 專案時,會出現以下的錯誤訊息:</p><blockquote><p>The “GenerateStaticWebAssetEndpointsPropsFile” task failed unexpectedly. System.IO.DirectoryNotFoundException: Could not find a part of the path</p></blockquote><h3 id="解法"><a href="#解法" class="headerlink" title="解法"></a>解法</h3><p>這通常是因為整個專案的路徑過長(超過 260 ),所以可以參考 <a href="https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?utm_source=chatgpt.com&tabs=registry">Maximum Path Length Limitation</a> 去<strong>Enable long paths</strong>,例如加 Windows 機碼的設定,如下:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">New-ItemProperty</span> <span class="literal">-Path</span> <span class="string">"HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem"</span> <span class="literal">-Name</span> <span class="string">"LongPathsEnabled"</span> <span class="literal">-Value</span> <span class="number">1</span> <span class="literal">-PropertyType</span> DWORD <span class="literal">-Force</span></span><br></pre></td></tr></table></figure><h3 id="參考資源"><a href="#參考資源" class="headerlink" title="參考資源"></a>參考資源</h3><p><a href="https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?utm_source=chatgpt.com&tabs=registry">Maximum Path Length Limitation</a></p>]]></content>
<summary type="html"><h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>在 .NET Build 專案時,會出現以下的錯誤訊息:</p>
<blockquote>
<p>The “GenerateStaticWe</summary>
<category term=".NET" scheme="https://rainmakerho.github.io/tags/NET/"/>
<category term="DirectoryNotFoundException" scheme="https://rainmakerho.github.io/tags/DirectoryNotFoundException/"/>
<category term="long paths" scheme="https://rainmakerho.github.io/tags/long-paths/"/>
<category term="260" scheme="https://rainmakerho.github.io/tags/260/"/>
<category term="GenerateStaticWebAssetEndpointsPropsFile" scheme="https://rainmakerho.github.io/tags/GenerateStaticWebAssetEndpointsPropsFile/"/>
<category term="路徑過長" scheme="https://rainmakerho.github.io/tags/%E8%B7%AF%E5%BE%91%E9%81%8E%E9%95%B7/"/>
</entry>
<entry>
<title>Dapper 使用 DynamicParameters 被 Checkmarx 掃出 SQL Injection 的問題與解法</title>
<link href="https://rainmakerho.github.io/2025/09/12/checkmarx-dapper-dynamicparameters-sqlinjection/"/>
<id>https://rainmakerho.github.io/2025/09/12/checkmarx-dapper-dynamicparameters-sqlinjection/</id>
<published>2025-09-12T06:28:17.000Z</published>
<updated>2025-09-12T06:50:28.296Z</updated>
<content type="html"><![CDATA[<h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>Checkmarx 升級到 V9.6.7.1005 HF20 後, 原本使用 Dapper 的程式居然被掃出有 <strong>SQL Injection</strong> 的風險。<br>程式以 Console 程式來說明,大約如下,</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">string</span> dbFile = <span class="string">"mydb.sqlite"</span>;</span><br><span class="line"><span class="built_in">string</span> connectionString = <span class="string">$"Data Source=<span class="subst">{dbFile}</span>"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">var</span> connection = <span class="keyword">new</span> SqliteConnection(connectionString);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 查詢 table_1</span></span><br><span class="line"><span class="built_in">string</span> sql = <span class="string">"SELECT Id, Name FROM table_1 WHERE Name=@name"</span>;</span><br><span class="line"><span class="built_in">string</span> name = args[<span class="number">0</span>];</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> p1 = <span class="keyword">new</span> DynamicParameters();</span><br><span class="line">p1.Add(<span class="string">"name"</span>, name);</span><br><span class="line">IEnumerable<Table1> results = connection.Query<Table1>(sql, p1);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Table1</span></span><br><span class="line">{</span><br><span class="line"> <span class="keyword">public</span> <span class="built_in">int</span> Id { <span class="keyword">get</span>; <span class="keyword">set</span>; }</span><br><span class="line"> <span class="keyword">public</span> <span class="built_in">string</span> Name { <span class="keyword">get</span>; <span class="keyword">set</span>; }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在 <code>IEnumerable<Table1> results = connection.Query<Table1>(sql, p1);</code> 會被 Checkmarx 掃出有 <strong>SQL Injection</strong> 的風險。</p><h3 id="解法"><a href="#解法" class="headerlink" title="解法"></a>解法</h3><p>經過多方的測試,發現 Checkmarx 並不認得上述的那種做法,所以就改用匿名物件放在<strong>Query</strong>的第二個參數之中才會 Pass ,如下,</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">IEnumerable<Table1> results = connection.Query<Table1>(sql, <span class="keyword">new</span> { name });</span><br></pre></td></tr></table></figure><ul><li>註: 感謝同事 unciax_wu 的幫忙</li></ul><h3 id="參考資訊"><a href="#參考資訊" class="headerlink" title="參考資訊"></a>參考資訊</h3><p><a href="https://www.learndapper.com/parameters">Using Parameters With Dapper</a></p>]]></content>
<summary type="html"><h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>Checkmarx 升級到 V9.6.7.1005 HF20 後, 原本使用 Dapper 的程式居然被掃出有 <strong>SQL In</summary>
<category term="Checkmarx" scheme="https://rainmakerho.github.io/tags/Checkmarx/"/>
<category term="SQL Injection" scheme="https://rainmakerho.github.io/tags/SQL-Injection/"/>
<category term="Dapper" scheme="https://rainmakerho.github.io/tags/Dapper/"/>
<category term="DynamicParameters" scheme="https://rainmakerho.github.io/tags/DynamicParameters/"/>
<category term="V9.6.7" scheme="https://rainmakerho.github.io/tags/V9-6-7/"/>
</entry>
<entry>
<title>Teams Bot 發送訊息給 Teams User 回傳 401 錯誤解決方案</title>
<link href="https://rainmakerho.github.io/2025/09/12/teams-bot-post-message-401/"/>
<id>https://rainmakerho.github.io/2025/09/12/teams-bot-post-message-401/</id>
<published>2025-09-12T01:25:01.000Z</published>
<updated>2025-09-12T02:06:57.109Z</updated>
<content type="html"><![CDATA[<h3 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h3><p>2025/7/31 後,Azure Bot 不再支援 Multi Tenant App,導致 Teams Bot 發送訊息時可能回傳 401 Unauthorized 錯誤。本文說明原因與解決方法。</p><h3 id="問題描述"><a href="#問題描述" class="headerlink" title="問題描述"></a>問題描述</h3><p>最近在建立 Teams Bot 後,透 Bot App 發送訊息給使用者時,會發生 <strong>401 Unauthorized</strong> 的錯誤。錯誤訊息如下:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Request failed with status code 401</span><br></pre></td></tr></table></figure><h3 id="環境說明"><a href="#環境說明" class="headerlink" title="環境說明"></a>環境說明</h3><p>Teams Bot 是透過在 Portal Microsoft Entra ID 中的 App 身份來發送訊息,<br>以往我們都是先在 App registrations 中註冊 App (設定為 <strong>Multitenant</strong>),<br>然後在<strong>Azure Bot</strong>中設定<strong>Microsoft App ID</strong>,<br>在裡面的<strong>Type of App</strong>也一併設定成<strong>Multi Tenant</strong>,<br>再選擇前面建立的 App。</p><h3 id="問題分析"><a href="#問題分析" class="headerlink" title="問題分析"></a>問題分析</h3><p>最近在<strong>Azure Bot</strong>中的<strong>Type of App</strong>卻只剩下<strong>Single Tenant</strong>及<strong>User-Assigned Managed Identity</strong>,<br><strong>Multi Tenant</strong>不見了!!! 如下圖:</p><img src="/2025/09/12/teams-bot-post-message-401/01.png" class="" title="Type of App"><p>而在<a href="https://learn.microsoft.com/en-us/azure/bot-service/bot-service-quickstart-registration?view=azure-bot-service-4.0&viewFallbackFrom=azure-bot-service-3.0&tabs=userassigned">Register a bot with Azure</a>中有備註</p><blockquote><p>Multi-tenant bot 在 2025/7/31 後就不能用了,如果在這之前建立的一樣可以用,但在 2025/7/31 後就不能用了~~</p></blockquote><p><strong>Multi Tenant App</strong>它取得 Token 的 URL 是 <code>https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token</code>,<br><strong>Single Tenant App</strong>它取得 Token 的 URL 是 <code>https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token</code></p><p>所以拿 <strong>Multi Tenant</strong>的 Token 去送訊息自然就會驗證錯誤,然後回<strong>401</strong>的錯誤。</p><p>所以<strong>2025/7/31</strong>之後 App 就建立要建立為<strong>Single Tenant</strong>哦~<br>目前發現,不管是 Single Tenant or Multi Tenant ,只要是<strong>Single Tenant</strong>的 Token 就可以順利發送訊息。</p><h3 id="結論"><a href="#結論" class="headerlink" title="結論"></a>結論</h3><p>2025/7/31 後,Azure Bot 必須使用 Single Tenant App。遇到 401 錯誤時,請檢查 Token 是否為 Single Tenant 的 Token。</p><h3 id="參考資源"><a href="#參考資源" class="headerlink" title="參考資源"></a>參考資源</h3><p><a href="https://learn.microsoft.com/en-us/azure/bot-service/bot-service-quickstart-registration?view=azure-bot-service-4.0&viewFallbackFrom=azure-bot-service-3.0&tabs=userassigned">Register a bot with Azure</a></p>]]></content>
<summary type="html"><h3 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h3><p>2025&#x2F;7&#x2F;31 後,Azure Bot 不再支援 Multi Tenant App,導致 Teams Bot 發送訊</summary>
<category term="Unauthorized" scheme="https://rainmakerho.github.io/tags/Unauthorized/"/>
<category term="Teams" scheme="https://rainmakerho.github.io/tags/Teams/"/>
<category term="Bot" scheme="https://rainmakerho.github.io/tags/Bot/"/>
<category term="API" scheme="https://rainmakerho.github.io/tags/API/"/>
<category term="Azure Bot" scheme="https://rainmakerho.github.io/tags/Azure-Bot/"/>
<category term="Single Tenant" scheme="https://rainmakerho.github.io/tags/Single-Tenant/"/>
<category term="401" scheme="https://rainmakerho.github.io/tags/401/"/>
<category term="Multi Tenant" scheme="https://rainmakerho.github.io/tags/Multi-Tenant/"/>
<category term="Microsoft Entra ID" scheme="https://rainmakerho.github.io/tags/Microsoft-Entra-ID/"/>
<category term="Token" scheme="https://rainmakerho.github.io/tags/Token/"/>
</entry>
<entry>
<title>使用 C# 和 Semantic Kernel 打造 AI 應用:第二章 - 深入 Plugins</title>
<link href="https://rainmakerho.github.io/2025/09/02/semantic-kernel-plugins-csharp/"/>
<id>https://rainmakerho.github.io/2025/09/02/semantic-kernel-plugins-csharp/</id>
<published>2025-09-02T00:44:21.000Z</published>
<updated>2025-09-02T00:58:21.152Z</updated>
<content type="html"><![CDATA[<h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>在上一章中,我們初探了 Semantic Kernel 的核心概念,並學習如何透過 Kernel 輕鬆地將 AI 服務整合到你的 C# 應用程式中。我們看到了 AI 基礎能力的強大,也理解到單靠 LLM 本身的知識庫仍有其限制。</p><p>這個限制,正是本章將要解決的核心問題。</p><p>想像一下,如果 AI 不僅能回答你提出的問題,還能執行更複雜的任務,例如:查詢即時的天氣、從資料庫中提取特定資訊,或是自動發送一封電子郵件。這將大大提升 AI 的應用潛力。</p><p>Semantic Kernel 提供了強大的 Plugins(插件) 機制,就像為你的 AI 助理裝備了一整個「工具箱」,讓它不再只是個知識淵博的大腦,更能動手執行任務,成為能解決現實問題的超能力者。<br>本章,我們將深入探索 Plugins 的世界,從最基礎的內建函數 (Native Functions) 開始,手把手帶你學習如何創建和使用自己的工具,並進一步了解如何透過檔案式提示 (File-based Prompts),將 AI 的能力提升到一個全新的層次。</p><h3 id="Using-a-Function-with-a-Complex-Type-Parameter-and-Return-Type"><a href="#Using-a-Function-with-a-Complex-Type-Parameter-and-Return-Type" class="headerlink" title="Using a Function with a Complex Type Parameter and Return Type"></a>Using a Function with a Complex Type Parameter and Return Type</h3><p>以下我們就來建立一個包含地點及日期的參數,做為查詢天氣預報的參數,試看看 LLM 是否能順利地從使用者的問題中,取出合適的參數來呼叫天氣預報的 Function ,如下:</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.SemanticKernel;</span><br><span class="line"><span class="keyword">using</span> System.ComponentModel;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">WeatherPlugin</span></span><br><span class="line">{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">readonly</span> <span class="built_in">string</span>[] Conditions = { <span class="string">"晴天"</span>,<span class="string">"多雲"</span>,<span class="string">"下雨"</span>, <span class="string">"豪雨"</span> };</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">readonly</span> Random Random = <span class="keyword">new</span> Random();</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> [<span class="meta">KernelFunction</span>]</span><br><span class="line"> [<span class="meta">Description(<span class="string">"取得特定日期及特定地點的天氣預測"</span>)</span>]</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="built_in">string</span> <span class="title">GetWeatherForecastForLocationAndDate</span>(<span class="params">[Description(<span class="string">"取得特定日期及特定地點的天氣預測的參數"</span></span>)] WeatherRequest weatherRequest)</span></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">var</span> condition = Conditions[Random.Next(Conditions.Length)];</span><br><span class="line"> <span class="keyword">var</span> highTemp = Random.Next(<span class="number">0</span>, <span class="number">40</span>);</span><br><span class="line"> <span class="keyword">var</span> lowTemp = Random.Next(<span class="number">10</span>, <span class="number">20</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="string">$"在 <span class="subst">{weatherRequest.Date.ToShortDateString()}</span>,<span class="subst">{weatherRequest.Location}</span> 的天氣預測是 <span class="subst">{condition}</span>,氣溫方面最高 <span class="subst">{highTemp}</span>°F,最低 <span class="subst">{lowTemp}</span>°F。"</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">WeatherRequest</span></span><br><span class="line">{</span><br><span class="line"> [<span class="meta">Description(<span class="string">"查詢天氣的地點"</span>)</span>]</span><br><span class="line"> <span class="keyword">public</span> <span class="built_in">string</span> Location { <span class="keyword">get</span>; <span class="keyword">set</span>; }</span><br><span class="line"></span><br><span class="line"> [<span class="meta">Description(<span class="string">"查詢天氣的日期"</span>)</span>]</span><br><span class="line"> <span class="keyword">public</span> DateTime Date { <span class="keyword">get</span>; <span class="keyword">set</span>; }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>然後在 Kernel 中註冊這個 Plugin 後,詢問<code>今2024/9/1 在台中的天氣如何?</code>,如下:</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.SemanticKernel;</span><br><span class="line"><span class="keyword">using</span> System.Text;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> apikey = <span class="string">"你的 openai api key"</span>;</span><br><span class="line"><span class="keyword">var</span> modelId = <span class="string">"gpt-4.1"</span>;</span><br><span class="line"><span class="comment">// 建立 KernelBuilder</span></span><br><span class="line"><span class="keyword">var</span> kernelBuilder = Kernel.CreateBuilder();</span><br><span class="line"><span class="comment">// 加入 OpenAI ChatCompletion 服務</span></span><br><span class="line"><span class="keyword">var</span> kernel = kernelBuilder</span><br><span class="line"> .AddOpenAIChatCompletion(modelId, apikey)</span><br><span class="line"> .Build();</span><br><span class="line"><span class="comment">// 註冊 plugin</span></span><br><span class="line">kernel.ImportPluginFromType<WeatherPlugin>();</span><br><span class="line">OpenAIPromptExecutionSettings settings = <span class="keyword">new</span>() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions };</span><br><span class="line">Console.OutputEncoding = UTF8Encoding.UTF8;</span><br><span class="line">Console.InputEncoding = UTF8Encoding.UTF8;</span><br><span class="line"><span class="comment">// 直接輸出結果</span></span><br><span class="line">Console.WriteLine(<span class="keyword">await</span> kernel.InvokePromptAsync(<span class="string">"今2024/9/1 在台中的天氣如何?"</span>, <span class="keyword">new</span>(settings)));</span><br></pre></td></tr></table></figure><p>在 <code>GetWeatherForecastForLocationAndDate</code> Method 設定中斷點,可以看到 LLM 從使用者的問題中取出<strong>地點</strong>及<strong>日期</strong>組成<code>WeatherRequest</code>參數傳到 Method 之中,如下圖:</p><img src="/2025/09/02/semantic-kernel-plugins-csharp/01.png" class="" title="Complex Type"><h3 id="Built-in-Plugins"><a href="#Built-in-Plugins" class="headerlink" title="Built-in Plugins"></a>Built-in Plugins</h3><p>Semantic kernel 核心的 Plugin 包含<code>Time plugin</code>、<code>HTTP plugin</code>、<code>FileIO plugin</code>、<code>ConversationSummary plugin</code>及<code>Text plugin</code>等等,<br>想知道更多,可以查看 <a href="https://github.com/microsoft/semantic-kernel/tree/main/dotnet/src/Plugins">Semantic kernel Plugins</a></p><p>1.加入 <code>Microsoft.SemanticKernel.Plugins.Core</code> Nuget 套件(preview)</p><p>2.使用<code>ConversationSummary plugin</code>, 整理一下對話內容,如下,</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.SemanticKernel;</span><br><span class="line"><span class="keyword">using</span> System.Text;</span><br><span class="line"><span class="keyword">using</span> Microsoft.SemanticKernel.Plugins.Core;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> apikey = <span class="string">"你的 openai api key"</span>;</span><br><span class="line"><span class="keyword">var</span> modelId = <span class="string">"gpt-4.1"</span>;</span><br><span class="line"><span class="comment">// 建立 KernelBuilder</span></span><br><span class="line"><span class="keyword">var</span> kernelBuilder = Kernel.CreateBuilder();</span><br><span class="line"><span class="comment">// 加入 OpenAI ChatCompletion 服務</span></span><br><span class="line"><span class="keyword">var</span> kernel = kernelBuilder</span><br><span class="line"> .AddOpenAIChatCompletion(modelId, apikey)</span><br><span class="line"> .Build();</span><br><span class="line"><span class="comment">// 註冊 plugin</span></span><br><span class="line">kernel.ImportPluginFromType<ConversationSummaryPlugin>();</span><br><span class="line">OpenAIPromptExecutionSettings settings = <span class="keyword">new</span>() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions };</span><br><span class="line">Console.OutputEncoding = UTF8Encoding.UTF8;</span><br><span class="line">Console.InputEncoding = UTF8Encoding.UTF8;</span><br><span class="line"><span class="keyword">var</span> chatTranscript = <span class="string">""</span><span class="string">"</span></span><br><span class="line"><span class="string">George:Mary,我在想我們是不是應該跟銀行借點錢來擴大生意。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Mary:嗯,我也有這個想法。不過你打算借多少?</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">George:大概新台幣兩百萬,主要是要添購新的設備。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Mary:兩百萬不算小數目,你覺得我們的還款能力足夠嗎?</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">George:以目前的營收,加上新設備帶來的產能,應該能在五年內還清。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Mary:那利率部分你有查過嗎?現在銀行大概是年利率 2.5% 到 3%。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">George:我看過一些資料,如果有公司財報跟資產擔保,應該可以談到 2.2% 左右。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Mary:聽起來不錯,但我們還需要準備好貸款計畫書,銀行才會比較容易審核。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">George:對,我打算先整理最近三年的財務報表,還有未來三年的營運計畫。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Mary:好,那我來準備市場分析的部分,讓銀行看到我們成長的潛力。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">George:太好了!等資料都準備好,我們就一起去銀行洽談吧。</span></span><br><span class="line"><span class="string">"</span><span class="string">""</span>;</span><br><span class="line">Console.WriteLine(<span class="keyword">await</span> kernel.InvokePromptAsync(<span class="string">$"請總結以下的對話內容,<span class="subst">{chatTranscript}</span>"</span>, <span class="keyword">new</span>(settings)));</span><br></pre></td></tr></table></figure><p>輸出結果如下:</p><img src="/2025/09/02/semantic-kernel-plugins-csharp/02.png" class="" title="ConversationSummary"><h3 id="File-based-Prompt-Functions-prompt-plugin"><a href="#File-based-Prompt-Functions-prompt-plugin" class="headerlink" title="File-based Prompt Functions(prompt plugin)"></a>File-based Prompt Functions(prompt plugin)</h3><p>prompt plugin 包含 <strong>skprompt.txt</strong> 及 <strong>config.json</strong><br>skprompt.txt: prompt 的內容(可包含變數)<br>config.json: prompt 的描述、執行的設定及變數的說明<br>以下使用人民陳情公文生成的例子來測試,找到相似的公文來生成來依使用者的陳情內容來生成新的公文,如下: 1.建立<code>Prompts\ComposeGovDoc</code>目錄</p><p>2.在目錄中新增<code>skprompt.txt</code></p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line">你是台灣政府機關的公文撰擬專家,需根據人民陳情與過往相似公文,產出正式可用的「{{$doc_type}}」。</span><br><span class="line"></span><br><span class="line">【輸入資料】</span><br><span class="line">- 陳情內容:</span><br><span class="line">{{$user_complaint}}</span><br><span class="line"></span><br><span class="line">- 檢索到的相似公文片段(供參考,避免照抄,需改寫與去個資):</span><br><span class="line">{{$similar_examples}}</span><br><span class="line"></span><br><span class="line">【寫作規範】</span><br><span class="line">1. 採用台灣公文常用結構:「主旨/說明/辦法」。</span><br><span class="line">2. 用語正式、精簡、條列清楚;避免口語與情緒性文字。</span><br><span class="line">3. 優先參考相似公文的處理邏輯,但需以本案事實改寫;不可捏造未提供的事證。</span><br><span class="line">4. 有缺資料時,以「請查明…」、「請會同…」等措辭引導,勿編造細節。</span><br><span class="line">5. 保護個資:姓名、電話、住址等以「[個資已遮蔽]」表述。</span><br><span class="line">6. 若涉及工務、照明、交通等,務必指明承辦單位(如:{{$target_agency}})與回覆時程(例如:{{$deadline_days}}日內)。</span><br><span class="line">7. 產出兩個部分:</span><br><span class="line"> A) JSON(機器可讀結構)</span><br><span class="line"> B) 正式正文(人工可讀)</span><br><span class="line"></span><br><span class="line">【JSON 輸出格式】(請只輸出有效 JSON 物件,不要加註解)</span><br><span class="line">{</span><br><span class="line"> "doc_type": "{{$doc_type}}",</span><br><span class="line"> "subject": "…(一句話說明主旨)",</span><br><span class="line"> "facts": [</span><br><span class="line"> "…(歸納已知事實1)",</span><br><span class="line"> "…(歸納已知事實2)"</span><br><span class="line"> ],</span><br><span class="line"> "actions": [</span><br><span class="line"> "…(應辦事項1)",</span><br><span class="line"> "…(應辦事項2)"</span><br><span class="line"> ],</span><br><span class="line"> "target_agency": "{{$target_agency}}",</span><br><span class="line"> "deadline_days": {{$deadline_days}},</span><br><span class="line"> "legal_basis": ["…(如有法源依據,無則留空陣列)"]</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">【正文輸出格式】</span><br><span class="line">主旨:……。</span><br><span class="line">說明:</span><br><span class="line">一、……。</span><br><span class="line">二、……。</span><br><span class="line">辦法:</span><br><span class="line">一、請{{$target_agency}}……。</span><br><span class="line">二、請於{{$deadline_days}}日內將辦理情形回復並副知本府。</span><br><span class="line"></span><br><span class="line">【產出】</span><br><span class="line">請先輸出 JSON,緊接著輸出「正文」。兩者中間空一行。</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>3.在目錄中新增<code>config.json</code></p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"schema"</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"description"</span><span class="punctuation">:</span> <span class="string">"依人民陳情內容與相似公文,擬出正式可用的台灣公文。"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"execution_settings"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"default"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"max_tokens"</span><span class="punctuation">:</span> <span class="number">1024</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"temperature"</span><span class="punctuation">:</span> <span class="number">0.3</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"></span><br><span class="line"> <span class="attr">"input_variables"</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"name"</span><span class="punctuation">:</span> <span class="string">"user_complaint"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"description"</span><span class="punctuation">:</span> <span class="string">"人民陳情的原始文字"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"default"</span><span class="punctuation">:</span> <span class="string">""</span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"name"</span><span class="punctuation">:</span> <span class="string">"similar_examples"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"description"</span><span class="punctuation">:</span> <span class="string">"相似公文片段"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"default"</span><span class="punctuation">:</span> <span class="string">""</span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"name"</span><span class="punctuation">:</span> <span class="string">"doc_type"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"description"</span><span class="punctuation">:</span> <span class="string">"公文文別"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"default"</span><span class="punctuation">:</span> <span class="string">"函"</span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"name"</span><span class="punctuation">:</span> <span class="string">"target_agency"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"description"</span><span class="punctuation">:</span> <span class="string">"承辦單位"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"default"</span><span class="punctuation">:</span> <span class="string">""</span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"name"</span><span class="punctuation">:</span> <span class="string">"deadline_days"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"description"</span><span class="punctuation">:</span> <span class="string">"回覆期限天數"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"default"</span><span class="punctuation">:</span> <span class="string">"14"</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"> <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></table></figure><p>4.設定<code>skprompt.txt</code>及<code>config.json</code> Copy 到 Output 目錄</p><p>接下來,就來看看程式如何使用<strong>prompt plugin</strong>, 如下:</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.SemanticKernel;</span><br><span class="line"><span class="keyword">using</span> System.Text;</span><br><span class="line"><span class="keyword">using</span> Microsoft.SemanticKernel.Plugins.Core;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> apikey = <span class="string">"你的 openai api key"</span>;</span><br><span class="line"><span class="keyword">var</span> modelId = <span class="string">"gpt-4.1"</span>;</span><br><span class="line"><span class="comment">// 建立 KernelBuilder</span></span><br><span class="line"><span class="keyword">var</span> kernelBuilder = Kernel.CreateBuilder();</span><br><span class="line"><span class="comment">// 加入 OpenAI ChatCompletion 服務</span></span><br><span class="line"><span class="keyword">var</span> kernel = kernelBuilder</span><br><span class="line"> .AddOpenAIChatCompletion(modelId, apikey)</span><br><span class="line"> .Build();</span><br><span class="line">OpenAIPromptExecutionSettings settings = <span class="keyword">new</span>() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions };</span><br><span class="line">Console.OutputEncoding = UTF8Encoding.UTF8;</span><br><span class="line">Console.InputEncoding = UTF8Encoding.UTF8;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 註冊 plugin</span></span><br><span class="line"><span class="keyword">var</span> prompts = kernel.CreatePluginFromPromptDirectory(<span class="string">"Prompts"</span>);</span><br><span class="line"><span class="keyword">var</span> fun = prompts[<span class="string">"ComposeGovDoc"</span>];</span><br><span class="line"><span class="comment">// 民眾陳情內容</span></span><br><span class="line"><span class="keyword">var</span> userMessage = <span class="string">@"陳情人:陳芊城陳述,在114年7月1日,在台北市林森北路52號巷口路燈不亮"</span>;</span><br><span class="line"><span class="comment">// 透過 RAG 取出相似的公文內容</span></span><br><span class="line"><span class="keyword">var</span> ragResult = <span class="string">@"主旨:關於民眾反映里內路燈故障一案,請查照。</span></span><br><span class="line"><span class="string">說明:</span></span><br><span class="line"><span class="string">一、依據民眾於113年9月1日陳情台北市承德路三段199號巷口路燈不亮。</span></span><br><span class="line"><span class="string">二、經查現場路燈編號A12、A13不亮。</span></span><br><span class="line"><span class="string">辦法:請工務課儘速派員檢修,完成後回復。"</span>;</span><br><span class="line"><span class="keyword">var</span> arguments = <span class="keyword">new</span> KernelArguments</span><br><span class="line">{</span><br><span class="line"> [<span class="string">"user_complaint"</span>] = userMessage,</span><br><span class="line"> [<span class="string">"similar_examples"</span>] = ragResult,</span><br><span class="line"> [<span class="string">"doc_type"</span>] = <span class="string">"函"</span>,</span><br><span class="line"> [<span class="string">"target_agency"</span>] = <span class="string">"工務課"</span>,</span><br><span class="line"> [<span class="string">"deadline_days"</span>] = <span class="string">"14"</span></span><br><span class="line">};</span><br><span class="line"><span class="comment">// 呼叫並傳入參數</span></span><br><span class="line"><span class="keyword">var</span> result = <span class="keyword">await</span> kernel.InvokeAsync(fun, arguments);</span><br><span class="line">Console.WriteLine(result);</span><br></pre></td></tr></table></figure><p>輸出結果如下:</p><img src="/2025/09/02/semantic-kernel-plugins-csharp/03.png" class="" title="File-based Prompt"><h3 id="Filters"><a href="#Filters" class="headerlink" title="Filters"></a>Filters</h3><p><a href="https://learn.microsoft.com/en-us/semantic-kernel/concepts/enterprise-readiness/filters?pivots=programming-language-csharp">Semantic Kernel Filters</a>提供三種類型:<br>Function Invocation Filter: 每次呼叫 <strong>KernelFunction</strong> 時,都會執行此過濾器。<br>Prompt Render Filter: 在將 prompt 給 AI 前,會執行此過濾器。<br>Auto Function Invocation Filter: 與<code>Function Invocation Filter</code>類似,提供額外的上下文信息(包含聊天歷史記錄、所有待執行函數的列表以及迭代計數器)。它還允許終止自動函數呼叫過程(<code>context.Terminate = true;</code>)<br>接下來使用<code>Function Invocation Filter</code>的程式碼會模擬一個對話流程,當 AI 試圖呼叫 ChangePrice 函式時,我們的 ApprovalFilter 會被觸發,並詢問使用者是否同意執行。<br>修改價格的 Plugin 及 Filter 如下:</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.SemanticKernel;</span><br><span class="line"><span class="keyword">using</span> System.ComponentModel;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">BookManagementPlugin</span></span><br><span class="line">{</span><br><span class="line"> [<span class="meta">KernelFunction</span>]</span><br><span class="line"> [<span class="meta">Description(<span class="string">"Change the price of a book in the database "</span>)</span>]</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">ChangePrice</span>(<span class="params">[Description(<span class="string">"The book id to update"</span></span>)] <span class="built_in">int</span> bookId, [<span class="title">Description</span>(<span class="params"><span class="string">"The new price"</span></span>)] <span class="built_in">int</span> newPrice)</span></span><br><span class="line"> {</span><br><span class="line"> <span class="comment">//update the price of the book in the database</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">ApprovalFilter</span>() : IFunctionInvocationFilter</span></span><br><span class="line">{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">async</span> Task <span class="title">OnFunctionInvocationAsync</span>(<span class="params">FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next</span>)</span></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">if</span> (context.Function.PluginName == <span class="string">"BookManagementPlugin"</span> && context.Function.Name == <span class="string">"ChangePrice"</span>)</span><br><span class="line"> {</span><br><span class="line"></span><br><span class="line"> Console.WriteLine(<span class="string">$"系統想要更新書本的價格,您要繼續嗎? (Y/N)"</span>);</span><br><span class="line"> <span class="built_in">string</span> shouldProceed = Console.ReadLine()!;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (shouldProceed != <span class="string">"Y"</span>)</span><br><span class="line"> {</span><br><span class="line"> context.Result = <span class="keyword">new</span> FunctionResult(context.Result, <span class="string">"價格變動未獲得批准"</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">await</span> next(context);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Console 程式如下:</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.SemanticKernel;</span><br><span class="line"><span class="keyword">using</span> System.Text;</span><br><span class="line"><span class="keyword">using</span> Microsoft.SemanticKernel.Plugins.Core;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> apikey = <span class="string">"你的 openai api key"</span>;</span><br><span class="line"><span class="keyword">var</span> modelId = <span class="string">"gpt-4.1"</span>;</span><br><span class="line"><span class="comment">// 建立 KernelBuilder</span></span><br><span class="line"><span class="keyword">var</span> kernelBuilder = Kernel.CreateBuilder();</span><br><span class="line"><span class="comment">// 註冊 Plugin</span></span><br><span class="line">kernelBuilder.Plugins.AddFromType<BookManagementPlugin>();</span><br><span class="line"><span class="comment">// 加入 ApprovalFilter</span></span><br><span class="line">kernelBuilder.Services.AddSingleton<IFunctionInvocationFilter, ApprovalFilter>();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 加入 OpenAI ChatCompletion 服務</span></span><br><span class="line"><span class="keyword">var</span> kernel = kernelBuilder</span><br><span class="line"> .AddOpenAIChatCompletion(modelId, apikey)</span><br><span class="line"> .Build();</span><br><span class="line">OpenAIPromptExecutionSettings settings = <span class="keyword">new</span>() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions };</span><br><span class="line">Console.OutputEncoding = UTF8Encoding.UTF8;</span><br><span class="line">Console.InputEncoding = UTF8Encoding.UTF8;</span><br><span class="line"><span class="keyword">var</span> chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();</span><br><span class="line">ChatHistory chatHistory = <span class="keyword">new</span>();</span><br><span class="line"><span class="built_in">string</span> userMessage = <span class="built_in">string</span>.Empty;</span><br><span class="line"><span class="keyword">while</span> (userMessage != <span class="string">"quit"</span>)</span><br><span class="line">{</span><br><span class="line"> Console.WriteLine(<span class="string">"Enter your question:"</span>);</span><br><span class="line"> userMessage = Console.ReadLine();</span><br><span class="line"> chatHistory.AddUserMessage(userMessage);</span><br><span class="line"> <span class="keyword">var</span> assistantMessage = <span class="keyword">await</span> chatCompletionService.GetChatMessageContentAsync(chatHistory, settings, kernel);</span><br><span class="line"> Console.WriteLine(assistantMessage);</span><br><span class="line"> chatHistory.Add(assistantMessage);</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>執行過程如下:</p><img src="/2025/09/02/semantic-kernel-plugins-csharp/04.png" class="" title="IFunctionInvocationFilter"><h3 id="Using-OpenAPI-Plugins"><a href="#Using-OpenAPI-Plugins" class="headerlink" title="Using OpenAPI Plugins"></a>Using OpenAPI Plugins</h3><p>企業可能有寫好的 API,只要有 OpenAPI 規格檔,用於描述其 API 的功能、參數、驗證方式等細節。Semantic Kernel 可以直接讀取這些規格檔,自動生成對應的函數,省下額外開發 AI Plugin 的時間。<br>以下以預設天氣 API 來測試, 1.加入 <code>Microsoft.SemanticKernel.Plugins.OpenApi</code> Nuget 套件 2.使用 <code>kernel.ImportPluginFromOpenApiAsync</code> 匯入 API 3.詢問 <code>今天天氣的如何</code> 就會呼叫 <code>GetWeatherForecast</code></p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.SemanticKernel;</span><br><span class="line"><span class="keyword">using</span> System.Text;</span><br><span class="line"><span class="keyword">using</span> Microsoft.SemanticKernel.Plugins.Core;</span><br><span class="line"><span class="keyword">using</span> Microsoft.SemanticKernel.Plugins.OpenApi;</span><br><span class="line"><span class="keyword">var</span> apikey = <span class="string">"你的 openai api key"</span>;</span><br><span class="line"><span class="keyword">var</span> modelId = <span class="string">"gpt-4.1"</span>;</span><br><span class="line"><span class="comment">// 建立 KernelBuilder</span></span><br><span class="line"><span class="keyword">var</span> kernelBuilder = Kernel.CreateBuilder();</span><br><span class="line"><span class="comment">// 加入 OpenAI ChatCompletion 服務</span></span><br><span class="line"><span class="keyword">var</span> kernel = kernelBuilder</span><br><span class="line"> .AddOpenAIChatCompletion(modelId, apikey)</span><br><span class="line"> .Build();</span><br><span class="line"><span class="comment">// 註冊 OpenAPI Plugin</span></span><br><span class="line"><span class="keyword">await</span> kernel.ImportPluginFromOpenApiAsync(</span><br><span class="line"> pluginName: <span class="string">"weatherforecast"</span>,</span><br><span class="line"> uri: <span class="keyword">new</span> Uri(<span class="string">"http://localhost:5129/openapi/v1.json"</span>),</span><br><span class="line"> executionParameters: <span class="keyword">new</span> OpenApiFunctionExecutionParameters()</span><br><span class="line"> {</span><br><span class="line"> EnablePayloadNamespacing = <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> );</span><br><span class="line">OpenAIPromptExecutionSettings settings = <span class="keyword">new</span>() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions };</span><br><span class="line">Console.OutputEncoding = UTF8Encoding.UTF8;</span><br><span class="line">Console.InputEncoding = UTF8Encoding.UTF8;</span><br><span class="line">Console.WriteLine(<span class="keyword">await</span> kernel.InvokePromptAsync(<span class="string">"今天天氣的如何"</span>, <span class="keyword">new</span>(settings)));</span><br><span class="line">Console.ReadLine();</span><br></pre></td></tr></table></figure><p>結果會輸出類似的結果 <code>根據今天的天氣預報,今天的氣溫約為23°C,天氣較冷。請注意保暖。</code></p><h3 id="結論"><a href="#結論" class="headerlink" title="結論"></a>結論</h3><p>在本章中,我們深入探索了 Semantic Kernel 的核心能力之一:Plugins。</p><p>我們從實例中見證了 Plugins 如何突破大型語言模型 (LLM) 的資訊限制,讓 AI 不僅能回答問題,更能與外部世界互動。無論是處理內建的複雜型別參數、利用現成的內建 Plugins,或是透過 File-based Prompts 來定義客製化的行為,Semantic Kernel 都提供了一套簡潔而強大的框架。此外,我們也了解了如何使用 Filters 來為 AI 流程加入額外的邏輯控制,以及如何將既有的 OpenAPI 規格快速轉化為可用的 Plugins,大幅提升開發效率。</p><p>透過這些功能,Semantic Kernel 賦予了我們為 LLM 打造專屬工具的能力,使 AI 應用程式變得更加智慧、靈活且具備處理現實世界任務的能力。<br>在下一章中,我們將深入探討 Semantic Kernel 如何讓你輕鬆地在不同的 LLM 服務提供者(例如 OpenAI、Azure OpenAI 等)之間切換,而無需修改核心程式碼。</p><h3 id="參考資源"><a href="#參考資源" class="headerlink" title="參考資源"></a>參考資源</h3><p><a href="https://github.com/microsoft/semantic-kernel">Semantic Kernel</a><br><a href="https://learn.microsoft.com/en-us/semantic-kernel/concepts/enterprise-readiness/filters?pivots=programming-language-csharp">Semantic Kernel Filters</a></p>]]></content>
<summary type="html"><h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>在上一章中,我們初探了 Semantic Kernel 的核心概念,並學習如何透過 Kernel 輕鬆地將 AI 服務整合到你的 C# 應用</summary>
<category term="C#" scheme="https://rainmakerho.github.io/tags/C/"/>
<category term=".NET" scheme="https://rainmakerho.github.io/tags/NET/"/>
<category term="Semantic Kernel" scheme="https://rainmakerho.github.io/tags/Semantic-Kernel/"/>
<category term="LLM" scheme="https://rainmakerho.github.io/tags/LLM/"/>
<category term="人工智慧" scheme="https://rainmakerho.github.io/tags/%E4%BA%BA%E5%B7%A5%E6%99%BA%E6%85%A7/"/>
<category term="Semantic Kernel Plugins" scheme="https://rainmakerho.github.io/tags/Semantic-Kernel-Plugins/"/>
<category term="AI Application" scheme="https://rainmakerho.github.io/tags/AI-Application/"/>
<category term="Native Functions" scheme="https://rainmakerho.github.io/tags/Native-Functions/"/>
<category term="File-based Prompts" scheme="https://rainmakerho.github.io/tags/File-based-Prompts/"/>
<category term="OpenAPI" scheme="https://rainmakerho.github.io/tags/OpenAPI/"/>
<category term="Filters" scheme="https://rainmakerho.github.io/tags/Filters/"/>
<category term="工具呼叫" scheme="https://rainmakerho.github.io/tags/%E5%B7%A5%E5%85%B7%E5%91%BC%E5%8F%AB/"/>
<category term="插件開發" scheme="https://rainmakerho.github.io/tags/%E6%8F%92%E4%BB%B6%E9%96%8B%E7%99%BC/"/>
</entry>
<entry>
<title>使用 C# 和 Semantic Kernel 打造 AI 應用:第一章 - 核心概念與 Plugins 介紹</title>
<link href="https://rainmakerho.github.io/2025/08/31/using-csharp-semantic-kernel-build-ai-apps-chapter-1-concepts-plugins/"/>
<id>https://rainmakerho.github.io/2025/08/31/using-csharp-semantic-kernel-build-ai-apps-chapter-1-concepts-plugins/</id>
<published>2025-08-31T12:23:45.000Z</published>
<updated>2025-08-31T12:40:59.987Z</updated>
<content type="html"><![CDATA[<h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>隨著大型語言模型 (LLM) 的崛起,人工智慧已不再是遙不可及的技術,而是正在深刻地影響著每個企業與工作流程。若想在 AI 浪潮中保持領先,最有效的方式就是將其應用於日常業務,實現流程自動化。</p><p>想像一下,過去需要耗費大量人力進行的客戶資料分類工作,比如綜合比對客戶的產業別、上市櫃資訊、營運項目,甚至是網路搜尋結果,現在都能藉由 AI 輕鬆完成。你只需上傳一份 Excel 文件,AI 便能自動處理大部分分類工作,員工只需專注於處理少數 AI 無法判斷的例外情況,大幅提升效率。</p><p>要將 AI 整合到現有應用程式 (AP) 中,我們可以透過多種方式,例如呼叫 AI 工具的 API,但當需求更複雜、需要更精確地控制整個過程時,直接呼叫 LLM 的 API 成了更好的選擇。<br>不過,這也帶來了新的挑戰:不同的 LLM 提供者 (如 OpenAI、Azure OpenAI) 有各自的 API 規範,開發者需要處理各種不同的參數設定、端點 (Endpoint) 和模型名稱 (Model Name)。<br>這種差異性不僅增加開發的複雜度,也讓未來切換或擴充 LLM 服務變得困難。</p><p>這正是 <a href="https://github.com/microsoft/semantic-kernel">Semantic Kernel</a> 派上用場的時候。作為一個由微軟開源的 SDK,Semantic Kernel 提供了一套統一且強大的框架,能夠協助開發者輕鬆、靈活地管理與 LLM 的互動。<br>它不僅簡化了與不同 LLM 服務的整合過程,更讓開發者能專注於打造具備「語義」理解能力的應用程式,而不用被底層技術細節所困擾。</p><p>在這個系列文章中,我們將深入探索如何使用 C# 和 Semantic Kernel,一步步構建具備 AI 智慧的應用。<br>我們將從基礎概念開始,逐步實作各種功能,帶你親身體驗 Semantic Kernel 如何幫助你輕鬆將 AI 能力整合至你的應用程式中。</p><h3 id="Semantic-Kernel"><a href="#Semantic-Kernel" class="headerlink" title="Semantic Kernel"></a>Semantic Kernel</h3><p>Semantic Kernel 是一個可擴充的輕量級的 .NET AI SDK,目標是讓 AP 可以輕易地與 AI 整合。<br>提供一個統一的介面,讓開發者可以用相同的方式去使用不同的 AI 服務(text generation, image geration, chat…),而不用在意每個服務的細節差異。<br>要快速掌握 Semantic Kernel 的核心,可以從它的六個主要元件開始理解:Kernel、AI Service Connectors、Functions and Plugins、Prompts and Prompt Templates、Memory 和 Filters。<br>以下我們建立 Console 程式來看看如何使用這些元件,</p><p>1.建立 Console App</p><p>2.加入 <code>Microsoft.SemanticKernel</code> Nuget 套件</p><p>3.準備好 OpenAI(或 AOAI, …) 的 API key</p><h3 id="Kernel-amp-AI-Service-Connectors"><a href="#Kernel-amp-AI-Service-Connectors" class="headerlink" title="Kernel & AI Service Connectors"></a>Kernel & AI Service Connectors</h3><p>在 Semantic Kernel 的世界裡,Kernel 扮演著核心中樞的角色。它不只負責串接應用程式和 AI 模型,更是所有 AI 服務與插件的協調者。想像它是一個大型工具箱,裡面裝滿了各式各樣的 AI 功能;Kernel 的任務就是確保這些工具隨時可用,讓開發者能隨心所欲地取用。<br>以下透過 <code>IKernelBuilder</code> 來建立 <code>Kernel</code> 後,讓使用者輸入訊息來與 LLM 對話,如下:</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.SemanticKernel;</span><br><span class="line"><span class="keyword">using</span> System.Text;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> apikey = <span class="string">"你的 openai api key"</span>;</span><br><span class="line"><span class="keyword">var</span> modelId = <span class="string">"gpt-4.1"</span>;</span><br><span class="line"><span class="comment">// 建立 KernelBuilder</span></span><br><span class="line"><span class="keyword">var</span> kernelBuilder = Kernel.CreateBuilder();</span><br><span class="line"><span class="comment">// 加入 OpenAI ChatCompletion 服務</span></span><br><span class="line"><span class="keyword">var</span> kernel = kernelBuilder</span><br><span class="line"> .AddOpenAIChatCompletion(modelId, apikey)</span><br><span class="line"> .Build();</span><br><span class="line">Console.OutputEncoding = UTF8Encoding.UTF8;</span><br><span class="line">Console.InputEncoding = UTF8Encoding.UTF8;</span><br><span class="line"><span class="built_in">string</span> userMessage = <span class="built_in">string</span>.Empty;</span><br><span class="line"><span class="comment">// 讓使用者輸入訊息來與 LLM 對話</span></span><br><span class="line"><span class="keyword">while</span>(userMessage != <span class="string">"quit"</span>)</span><br><span class="line">{</span><br><span class="line"> Console.WriteLine(<span class="string">"Enter you message:"</span>);</span><br><span class="line"> userMessage = Console.ReadLine();</span><br><span class="line"> Console.WriteLine(<span class="keyword">await</span> kernel.InvokePromptAsync(userMessage));</span><br><span class="line">}</span><br></pre></td></tr></table></figure><img src="/2025/08/31/using-csharp-semantic-kernel-build-ai-apps-chapter-1-concepts-plugins/01.png" class="" title="kernel.InvokePromptAsync"><p>接下來,讓使用者輸入訊息來產生圖片,如下:</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.SemanticKernel;</span><br><span class="line"><span class="keyword">using</span> System.Text;</span><br><span class="line"><span class="keyword">using</span> Microsoft.SemanticKernel.TextToImage;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> apikey = <span class="string">"你的 openai api key"</span>;</span><br><span class="line"><span class="keyword">var</span> imageModelId = <span class="string">"dall-e-3"</span>;</span><br><span class="line"><span class="comment">// 建立 KernelBuilder</span></span><br><span class="line"><span class="keyword">var</span> kernelBuilder = Kernel.CreateBuilder();</span><br><span class="line"><span class="comment">// 加入 OpenAI Text to image 服務</span></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> <span class="keyword">warning</span> disable SKEXP0010, SKEXP0001</span></span><br><span class="line"><span class="keyword">var</span> kernel = kernelBuilder</span><br><span class="line"> .AddOpenAITextToImage(modelId: imageModelId, apiKey: apikey)</span><br><span class="line"> .Build();</span><br><span class="line"><span class="comment">// 取得 TextToImage Service</span></span><br><span class="line">ITextToImageService imageService = kernel.GetRequiredService<ITextToImageService>();</span><br><span class="line"><span class="built_in">string</span> prompt =</span><br><span class="line"><span class="string">""</span><span class="string">"</span></span><br><span class="line"><span class="string">創建一幅逼真的圖片,描繪RM的咖啡店。這家店擁有迷人的鄉村外觀,紅磚外牆、大型玻璃窗,和在入口上方懸掛的經典木製招牌,</span></span><br><span class="line"><span class="string">上面用優雅的手繪字體寫著「RM's Coffee Shop」。入口處有一道復古風格的木製門,上面有一個小鈴鐺,以及門外顯示今日特價的黑板招牌,</span></span><br><span class="line"><span class="string">包括「柚香拿鐵」、「櫻桃咖啡」和「南瓜咖啡」。</span></span><br><span class="line"><span class="string">"</span><span class="string">""</span>;</span><br><span class="line"><span class="keyword">var</span> image = <span class="keyword">await</span> imageService.GenerateImageAsync(prompt, <span class="number">1792</span>, <span class="number">1024</span>);</span><br><span class="line">Console.WriteLine(<span class="string">"Image URL: "</span> + image);</span><br></pre></td></tr></table></figure><img src="/2025/08/31/using-csharp-semantic-kernel-build-ai-apps-chapter-1-concepts-plugins/02.png" class="" title="GenerateImageAsync"><h3 id="Functions-and-Plugins"><a href="#Functions-and-Plugins" class="headerlink" title="Functions and Plugins"></a>Functions and Plugins</h3><p>大型語言模型 (LLM) 雖然強大,但它們的知識庫僅限於訓練時所使用的資料。<br>這意味著,LLM 無法存取即時資訊,也無法執行外部系統中的特定操作,例如查詢資料庫、發送電子郵件或進行外部 API 呼叫。<br>這使得單純使用 LLM 的應用程式難以處理需要最新資訊或與外部世界互動的任務。<br>就像當我在上面圖片中,輸入<code>今天日期是?</code> LLM 回答是 <code>今天的日期是2023年6月13日。</code></p><p>Semantic Kernel 提供了一套函數 (Functions) 和插件 (Plugins),專門用來解決這個問題。<br>你可以將這些函數視為 LLM 的「工具」或「外掛」,讓 LLM 能夠:</p><ul><li>存取即時或私有資料:例如,查詢你的內部產品庫存、客戶資料,或是最新的天氣資訊。</li><li>執行特定操作:如自動發送通知郵件、在客戶關係管理 (CRM) 系統中創建新記錄,或在網路上搜尋特定內容。</li></ul><p>這些函數讓 LLM 不再受限於其訓練資料,而是能像一個聰明的代理人,在需要時調用適當的工具來完成任務。<br>以下我們就來建立一個<strong>Plugin</strong>,Function 設定<code>[KernelFunction]</code>屬性,並透過<code>Description</code>來說<strong>Function</strong>的用途,如下:</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.SemanticKernel;</span><br><span class="line"><span class="keyword">using</span> System.ComponentModel;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">TimePlugin</span></span><br><span class="line">{</span><br><span class="line"> [<span class="meta">KernelFunction</span>]</span><br><span class="line"> [<span class="meta">Description(<span class="string">"取得現在UTC的日期及時間"</span>)</span>]</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="built_in">string</span> <span class="title">GetCurrentDateAndTime</span>()</span></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">return</span> DateTime.UtcNow.ToString(<span class="string">"R"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>然後將它 Import 到 Kernel 中,並設定 LLM 自動執行 Function ,並詢問 LLM <code>今天日期是?</code>,就會回答正確的日期,而不再是之前的<code>2023年6月13日</code>,如下,</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.SemanticKernel;</span><br><span class="line"><span class="keyword">using</span> System.Text;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> apikey = <span class="string">"你的 openai api key"</span>;</span><br><span class="line"><span class="keyword">var</span> modelId = <span class="string">"gpt-4.1"</span>;</span><br><span class="line"><span class="comment">// 建立 KernelBuilder</span></span><br><span class="line"><span class="keyword">var</span> kernelBuilder = Kernel.CreateBuilder();</span><br><span class="line"><span class="comment">// 加入 OpenAI ChatCompletion 服務</span></span><br><span class="line"><span class="keyword">var</span> kernel = kernelBuilder</span><br><span class="line"> .AddOpenAIChatCompletion(modelId, apikey)</span><br><span class="line"> .Build();</span><br><span class="line"><span class="comment">// 註冊 plugin</span></span><br><span class="line">kernel.ImportPluginFromType<TimePlugin>();</span><br><span class="line">OpenAIPromptExecutionSettings settings = <span class="keyword">new</span>() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions };</span><br><span class="line">Console.OutputEncoding = UTF8Encoding.UTF8;</span><br><span class="line">Console.InputEncoding = UTF8Encoding.UTF8;</span><br><span class="line">Console.WriteLine(<span class="keyword">await</span> kernel.InvokePromptAsync(<span class="string">"今天日期是? "</span>, <span class="keyword">new</span>(settings)));</span><br></pre></td></tr></table></figure><p>Semantic Kernel 在呼叫 LLM 之前,會將你註冊的 Plugins 描述(包含其名稱、描述和參數)序列化後,作為提示 (prompt) 的一部分傳送給 LLM。如果你的 Plugins 數量過多或描述過於冗長,會佔用大量的提示令牌 (token),增加成本並可能導致提示被截斷。<br>每次呼叫 LLM 時,建議最多只使用 10 到 20 個。若超過這個數量,模型會難以準確地選擇和使用正確的工具,容易產生錯誤或不穩定的行為。</p><ul><li>註: Funciton Calling 的過程,可以參考 <code>https://platform.openai.com/docs/guides/function-calling</code> 的圖片來了解。</li></ul><img src="https://cdn.openai.com/API/docs/images/function-calling-diagram-steps.png" width="50%" height="50%" ><h3 id="結論"><a href="#結論" class="headerlink" title="結論"></a>結論</h3><p>在本篇文章中,我們一起探索了 Semantic Kernel 的核心概念,並透過實際範例,體驗了如何使用 Kernel 輕鬆串接多種 AI 服務(如文字生成與圖片生成)。<br>我們也看到,藉由 Plugins 如何突破 LLM 的資訊限制,讓 AI 能存取外部資料並執行真實世界的任務,將其從一個『資訊庫』轉變為一個『智慧代理人』。</p><p>在下一篇,我們將會深入探討 Plugins 的細節,包括如何建立和使用內建的 Plugins,以及如何利用 File-based Prompt Functions,讓你的應用程式具備更強大的語義理解與自動化能力。敬請期待!</p><h3 id="參考資源"><a href="#參考資源" class="headerlink" title="參考資源"></a>參考資源</h3><p><a href="https://github.com/microsoft/semantic-kernel">Semantic Kernel</a></p>]]></content>
<summary type="html"><h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>隨著大型語言模型 (LLM) 的崛起,人工智慧已不再是遙不可及的技術,而是正在深刻地影響著每個企業與工作流程。若想在 AI 浪潮中保持領先,</summary>
<category term="C#" scheme="https://rainmakerho.github.io/tags/C/"/>
<category term=".NET" scheme="https://rainmakerho.github.io/tags/NET/"/>
<category term="Plugins" scheme="https://rainmakerho.github.io/tags/Plugins/"/>
<category term="Semantic Kernel" scheme="https://rainmakerho.github.io/tags/Semantic-Kernel/"/>
<category term="OpenAI" scheme="https://rainmakerho.github.io/tags/OpenAI/"/>
<category term="LLM" scheme="https://rainmakerho.github.io/tags/LLM/"/>
<category term="AI 應用" scheme="https://rainmakerho.github.io/tags/AI-%E6%87%89%E7%94%A8/"/>
<category term="開源 SDK" scheme="https://rainmakerho.github.io/tags/%E9%96%8B%E6%BA%90-SDK/"/>
<category term="核心概念" scheme="https://rainmakerho.github.io/tags/%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5/"/>
<category term="語義核心" scheme="https://rainmakerho.github.io/tags/%E8%AA%9E%E7%BE%A9%E6%A0%B8%E5%BF%83/"/>
<category term="程式開發" scheme="https://rainmakerho.github.io/tags/%E7%A8%8B%E5%BC%8F%E9%96%8B%E7%99%BC/"/>
<category term="人工智慧" scheme="https://rainmakerho.github.io/tags/%E4%BA%BA%E5%B7%A5%E6%99%BA%E6%85%A7/"/>
<category term="函式呼叫" scheme="https://rainmakerho.github.io/tags/%E5%87%BD%E5%BC%8F%E5%91%BC%E5%8F%AB/"/>
</entry>
<entry>
<title>透過 Microsoft Graph API 取得 Teams 線上會議文字記錄完整教學</title>
<link href="https://rainmakerho.github.io/2025/08/21/microsoft-graph-api-get-teams-online-meeting-transcript/"/>
<id>https://rainmakerho.github.io/2025/08/21/microsoft-graph-api-get-teams-online-meeting-transcript/</id>
<published>2025-08-21T01:16:52.000Z</published>
<updated>2025-08-21T01:46:44.070Z</updated>
<content type="html"><![CDATA[<p>在 Teams 中啟用會議錄影時,系統同時也會產生會議的文字記錄 (Transcript)。<br>這些文字記錄不僅能協助參與者回顧內容,還能交給 GPT 或其他工具產生更完整的會議紀錄。<br>一般情況下,只有會議主持人才能手動下載這些文字記錄。<br>那麼,是否能讓應用程式自動存取並下載會議文字記錄呢?本文將逐步示範實作方式。</p><h3 id="實作"><a href="#實作" class="headerlink" title="實作"></a>實作</h3><h5 id="1-註冊要存取的-App"><a href="#1-註冊要存取的-App" class="headerlink" title="1.註冊要存取的 App"></a>1.註冊要存取的 App</h5><p>由於必須透過 AP 存取,因此需先註冊一個 Azure AD App。完成後建立 Client Secret,並保存其 Value,稍後將用於取得存取 Token。詳細可以參考<a href="https://rainmakerho.github.io/2022/04/29/teams-app-access-meetings-behalf-user/">Teams App 代替使用者建立線上會議,讓該使用者為會議主持人</a>的說明來註冊 App。</p><h4 id="2-設定-APP-需要的權限"><a href="#2-設定-APP-需要的權限" class="headerlink" title="2.設定 APP 需要的權限"></a>2.設定 APP 需要的權限</h4><p>需要 Microsoft Graph API, Type 為 Application 的以下權限,</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">User.ReadBasic.All</span><br><span class="line">Calendars.ReadBasic.All</span><br><span class="line">OnlineMeetings.Read.All</span><br><span class="line">OnlineMeetingTranscript.Read.All</span><br><span class="line">Team.ReadBasic.All</span><br><span class="line">TeamSettings.Read.All</span><br></pre></td></tr></table></figure><p>設定完成後,請記得該 Azure 管理者 按一下 <strong>Grant admin consent for …</strong> 允許 App 可以用這些 API,如下圖所示:</p><img src="/2025/08/21/microsoft-graph-api-get-teams-online-meeting-transcript/01.png" class="" title="api permissons"><h3 id="Console-程式碼實作"><a href="#Console-程式碼實作" class="headerlink" title="Console 程式碼實作"></a>Console 程式碼實作</h3><p>以下為 C# Console 的程式碼,先設定需要的變數,例如 clientId, clientSecret …</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> System.Dynamic;</span><br><span class="line"><span class="keyword">using</span> System.Net.Http.Headers;</span><br><span class="line"><span class="keyword">using</span> System.Text;</span><br><span class="line"><span class="keyword">using</span> System.Text.Json;</span><br><span class="line"><span class="keyword">using</span> System.Text.Json.Nodes;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> clientId = <span class="string">"{Application (client) ID}"</span>;</span><br><span class="line"><span class="keyword">var</span> clientSecret = <span class="string">"{clientSecret}"</span>;</span><br><span class="line"><span class="keyword">var</span> tenantId = <span class="string">"{Directory (tenant) ID}"</span>;</span><br><span class="line"><span class="keyword">var</span> userPrincipalName = <span class="string">"{通常是email}"</span>;</span><br><span class="line"></span><br><span class="line">Console.OutputEncoding = Encoding.UTF8;</span><br><span class="line"><span class="keyword">var</span> contentType = <span class="string">"application/json"</span>;</span><br><span class="line"><span class="keyword">var</span> client = <span class="keyword">new</span> HttpClient();</span><br><span class="line"></span><br></pre></td></tr></table></figure><h4 id="3-取得-App-的-access-token"><a href="#3-取得-App-的-access-token" class="headerlink" title="3.取得 App 的 access token"></a>3.取得 App 的 access token</h4><p>先取得 App 的 access token,用於呼叫 Graph API</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> getTokenUrl = <span class="string">$"https://login.microsoftonline.com/<span class="subst">{tenantId}</span>/oauth2/v2.0/token"</span>;</span><br><span class="line"><span class="keyword">var</span> authBody = <span class="string">$"grant_type=client_credentials&client_id=<span class="subst">{clientId}</span>&client_secret=<span class="subst">{clientSecret}</span>&scope=https://graph.microsoft.com/.default"</span>;</span><br><span class="line"><span class="keyword">var</span> authResponse = <span class="keyword">await</span> client.PostAsync(getTokenUrl, <span class="keyword">new</span> StringContent(authBody, Encoding.UTF8, <span class="string">"application/x-www-form-urlencoded"</span>));</span><br><span class="line"><span class="comment">//取得 AccessToken</span></span><br><span class="line"><span class="keyword">var</span> authResult = <span class="keyword">await</span> authResponse.Content.ReadAsStringAsync();</span><br><span class="line"><span class="keyword">var</span> options = <span class="keyword">new</span> JsonSerializerOptions</span><br><span class="line">{</span><br><span class="line"> PropertyNameCaseInsensitive = <span class="literal">true</span></span><br><span class="line">};</span><br><span class="line"><span class="built_in">dynamic</span> tokenResponse = JsonSerializer.Deserialize<ExpandoObject>(authResult, options);</span><br><span class="line"><span class="keyword">var</span> authObj = JsonNode.Parse(authResult);</span><br><span class="line">accessToken = (<span class="built_in">string</span>)authObj[<span class="string">"access_token"</span>];</span><br><span class="line"></span><br><span class="line">Console.WriteLine(<span class="string">"======== access Token ========="</span>);</span><br><span class="line">Console.WriteLine(accessToken);</span><br></pre></td></tr></table></figure><h4 id="4-取得使用者的-AAD-UserId"><a href="#4-取得使用者的-AAD-UserId" class="headerlink" title="4.取得使用者的 AAD UserId"></a>4.取得使用者的 AAD UserId</h4><p>呼叫 API 取得使用者的 Azure AD User Id (<code>User.ReadBasic.All 權限</code>)</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> getUserUrl = <span class="string">$"https://graph.microsoft.com/v1.0/users/<span class="subst">{userPrincipalName}</span>"</span>;</span><br><span class="line">client.DefaultRequestHeaders.Accept.Add(<span class="keyword">new</span> MediaTypeWithQualityHeaderValue(contentType));</span><br><span class="line">client.DefaultRequestHeaders.Add(<span class="string">"Authorization"</span>, <span class="string">$"Bearer <span class="subst">{accessToken}</span>"</span>);</span><br><span class="line"><span class="keyword">var</span> userResponse = <span class="keyword">await</span> client.GetAsync(getUserUrl);</span><br><span class="line"><span class="keyword">var</span> userResult = <span class="keyword">await</span> userResponse.Content.ReadAsStringAsync();</span><br><span class="line"><span class="keyword">var</span> userObj = JsonNode.Parse(userResult);</span><br><span class="line"><span class="keyword">var</span> userId = (<span class="built_in">string</span>)userObj[<span class="string">"id"</span>];</span><br><span class="line">Console.WriteLine(<span class="string">"======== Azure AD User Id ========="</span>);</span><br><span class="line">Console.WriteLine(userId);</span><br></pre></td></tr></table></figure><ul><li>註: 使用者必需要是會議的參與者,否則會取不到資料</li></ul><h4 id="5-依會議主旨來取得行事曆事件"><a href="#5-依會議主旨來取得行事曆事件" class="headerlink" title="5.依會議主旨來取得行事曆事件"></a>5.依會議主旨來取得行事曆事件</h4><p>依會議主旨查詢 Events,並取得 Join URL</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> searchSubject = <span class="string">"TID"</span>;</span><br><span class="line"><span class="keyword">var</span> getEventsBySubjectFilterUrl = <span class="string">$"https://graph.microsoft.com/v1.0/users/<span class="subst">{userId}</span>/events?$filter=startswith(subject, '<span class="subst">{searchSubject}</span>')"</span>;</span><br><span class="line"><span class="comment">//也可以用 日期 Filter</span></span><br><span class="line"><span class="comment">//var getEventsByDateFilterUrl = $"https://graph.microsoft.com/v1.0/users/{userId}/events?$filter=start/dateTime ge '2025/03/19' and start/dateTime le '2025/03/20' ";</span></span><br><span class="line"><span class="keyword">var</span> eventsResponse = <span class="keyword">await</span> client.GetAsync(getEventsBySubjectFilterUrl);</span><br><span class="line"><span class="keyword">var</span> eventsResult = <span class="keyword">await</span> eventsResponse.Content.ReadAsStringAsync();</span><br><span class="line"><span class="keyword">var</span> eventsObj = JsonNode.Parse(eventsResult);</span><br><span class="line"><span class="keyword">var</span> eventList = eventsObj[<span class="string">"value"</span>].AsArray();</span><br><span class="line">Console.WriteLine(<span class="string">"======== Events ========="</span>);</span><br><span class="line"><span class="keyword">foreach</span> (<span class="keyword">var</span> eventItem <span class="keyword">in</span> eventList)</span><br><span class="line">{</span><br><span class="line"> <span class="keyword">var</span> onlineMeeting = eventItem[<span class="string">"onlineMeeting"</span>];</span><br><span class="line"> <span class="keyword">if</span> (onlineMeeting != <span class="literal">null</span>)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">var</span> subject = eventItem[<span class="string">"subject"</span>].ToString();</span><br><span class="line"> Console.WriteLine(<span class="string">$"<span class="subst">{subject}</span>:<span class="subst">{onlineMeeting[<span class="string">"joinUrl"</span>].ToString()}</span>"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="6-透過線上會議的-Link-來取得線上會議的-Id"><a href="#6-透過線上會議的-Link-來取得線上會議的-Id" class="headerlink" title="6.透過線上會議的 Link 來取得線上會議的 Id"></a>6.透過線上會議的 Link 來取得線上會議的 Id</h4><p>如果一開始就有線上會議的 Link,就可以省略<strong>4.依會議主旨來取得行事曆事件</strong></p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> joinWebUrl = <span class="string">@"https://teams.microsoft.com/l/meetup-join/....."</span>;</span><br><span class="line"><span class="keyword">var</span> getOnlineMeetingsUrl = <span class="string">$"https://graph.microsoft.com/v1.0/users/<span class="subst">{userId}</span>/onlineMeetings?$filter=JoinWebUrl eq '<span class="subst">{joinWebUrl}</span>'"</span>;</span><br><span class="line"><span class="keyword">var</span> onlineMeetingsResponse = <span class="keyword">await</span> client.GetAsync(getOnlineMeetingsUrl);</span><br><span class="line"><span class="keyword">var</span> onlineMeetingsResult = <span class="keyword">await</span> onlineMeetingsResponse.Content.ReadAsStringAsync();</span><br><span class="line">Console.WriteLine(<span class="string">"======== Online Meeting Id ========="</span>);</span><br><span class="line"><span class="keyword">var</span> meetingsObj = JsonNode.Parse(onlineMeetingsResult);</span><br><span class="line"><span class="keyword">var</span> meetingId = <span class="string">""</span>;</span><br><span class="line"><span class="comment">//onlineMeetingResult</span></span><br><span class="line"><span class="keyword">if</span> (meetingsObj[<span class="string">"error"</span>] != <span class="literal">null</span>)</span><br><span class="line">{</span><br><span class="line"> Console.WriteLine(meetingsObj[<span class="string">"error"</span>][<span class="string">"message"</span>]);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">{</span><br><span class="line"> meetingId = (<span class="built_in">string</span>)meetingsObj[<span class="string">"value"</span>][<span class="number">0</span>][<span class="string">"id"</span>];</span><br><span class="line"> Console.WriteLine(meetingId);</span><br><span class="line"> Console.WriteLine((<span class="built_in">string</span>)meetingsObj[<span class="string">"value"</span>][<span class="number">0</span>][<span class="string">"subject"</span>]);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>註: 如果出現<code>No application access policy found for this app.</code>的錯誤,請依<a href="https://learn.microsoft.com/en-us/graph/cloud-communication-online-meeting-application-access-policy">Allow applications to access online meetings on behalf of a user</a>方式來設定讓 App 有權限去執行</li><li>註: 建議一開始先設定測試的使用者比較快生效,例如<code>Grant-CsApplicationAccessPolicy -PolicyName "teams-meetings-policy" -Identity "{userId}"</code>,如果設定<strong>Global</strong>需要等蠻久一段時間(超過 30 分鐘)才會生效</li></ul><h4 id="7-取得線上會議的文字記錄-多筆-的資訊"><a href="#7-取得線上會議的文字記錄-多筆-的資訊" class="headerlink" title="7.取得線上會議的文字記錄(多筆)的資訊"></a>7.取得線上會議的文字記錄(多筆)的資訊</h4><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> getMeetingTranscriptsUrl = <span class="string">$"https://graph.microsoft.com/v1.0/users/<span class="subst">{userId}</span>/onlineMeetings/<span class="subst">{meetingId}</span>/transcripts"</span>;</span><br><span class="line"><span class="keyword">var</span> meetingTranscriptsResponse = <span class="keyword">await</span> client.GetAsync(getMeetingTranscriptsUrl);</span><br><span class="line"><span class="keyword">var</span> meetingTranscriptsResult = <span class="keyword">await</span> meetingTranscriptsResponse.Content.ReadAsStringAsync();</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> meetingTranscriptsObj = JsonNode.Parse(meetingTranscriptsResult);</span><br><span class="line"><span class="keyword">var</span> transcriptsCount = meetingTranscriptsObj[<span class="string">"@odata.count"</span>];</span><br><span class="line">meetingTranscriptsObj[<span class="string">"value"</span>].AsArray();</span><br><span class="line"></span><br><span class="line">Console.WriteLine(<span class="string">$"Total Transcripts Count:<span class="subst">{transcriptsCount}</span>"</span>);</span><br><span class="line"><span class="comment">//the latest transcript</span></span><br><span class="line"><span class="keyword">var</span> lastTranscript = meetingTranscriptsObj[<span class="string">"value"</span>].AsArray().LastOrDefault();</span><br><span class="line"><span class="keyword">var</span> lastTranscriptId = (<span class="built_in">string</span>)lastTranscript[<span class="string">"id"</span>];</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>線上會議的文字記錄可能會有多筆,可以依<code>createdDateTime</code>過濾所需的記錄。本文示範取最後一筆作為測試。</p><h4 id="8-取得最後一筆線上會議的文字記錄"><a href="#8-取得最後一筆線上會議的文字記錄" class="headerlink" title="8.取得最後一筆線上會議的文字記錄"></a>8.取得最後一筆線上會議的文字記錄</h4><p>有了 TranscriptId 就可以取得文字記錄,格式選擇<code>text/vtt</code></p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> meetingTranscriptUrl = <span class="string">$"https://graph.microsoft.com/v1.0/users/<span class="subst">{userId}</span>/onlineMeetings/<span class="subst">{meetingId}</span>/transcripts('<span class="subst">{lastTranscriptId}</span>')/content?$format=text/vtt"</span>;</span><br><span class="line"><span class="keyword">var</span> meetingTranscriptResponse = <span class="keyword">await</span> client.GetAsync(meetingTranscriptUrl);</span><br><span class="line"><span class="keyword">var</span> meetingTranscriptResult = <span class="keyword">await</span> meetingTranscriptResponse.Content.ReadAsStringAsync();</span><br><span class="line">Console.WriteLine(<span class="string">$"=== Transcript ==============="</span>);</span><br><span class="line">Console.WriteLine(meetingTranscriptResult.Substring(<span class="number">0</span>, <span class="number">500</span>));</span><br></pre></td></tr></table></figure><h3 id="結論"><a href="#結論" class="headerlink" title="結論"></a>結論</h3><p>綜合以上步驟,我們可以透過 Microsoft Graph API 依主旨或日期找到會議事件,取得 Join URL,再進一步查詢 OnlineMeeting Id,最後存取該會議的 Transcript。<br>此流程能協助開發者自動化取得 Teams 線上會議的逐字稿,進一步應用於會議紀錄、智慧摘要或 NLP 分析。</p><h3 id="參考資源"><a href="#參考資源" class="headerlink" title="參考資源"></a>參考資源</h3><p><a href="https://rainmakerho.github.io/2022/04/29/teams-app-access-meetings-behalf-user/">Teams App 代替使用者建立線上會議,讓該使用者為會議主持人</a><br><a href="https://learn.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http">Microsoft Graph API Get a user</a><br><a href="https://learn.microsoft.com/en-us/graph/api/onlinemeeting-get?view=graph-rest-1.0&tabs=http">Microsoft Graph API Get onlineMeeting</a><br><a href="https://learn.microsoft.com/en-us/graph/cloud-communication-online-meeting-application-access-policy">Allow applications to access online meetings on behalf of a user</a></p>]]></content>
<summary type="html"><p>在 Teams 中啟用會議錄影時,系統同時也會產生會議的文字記錄 (Transcript)。<br>這些文字記錄不僅能協助參與者回顧內容,還能交給 GPT 或其他工具產生更完整的會議紀錄。<br>一般情況下,只有會議主持人才能手動下載這些文字記錄。<br>那麼,是否能讓應用</summary>
<category term="Microsoft Graph API" scheme="https://rainmakerho.github.io/tags/Microsoft-Graph-API/"/>
<category term="Teams 會議記錄" scheme="https://rainmakerho.github.io/tags/Teams-%E6%9C%83%E8%AD%B0%E8%A8%98%E9%8C%84/"/>
<category term="Teams Transcript" scheme="https://rainmakerho.github.io/tags/Teams-Transcript/"/>
<category term="Teams 線上會議" scheme="https://rainmakerho.github.io/tags/Teams-%E7%B7%9A%E4%B8%8A%E6%9C%83%E8%AD%B0/"/>
<category term="Graph API OnlineMeeting" scheme="https://rainmakerho.github.io/tags/Graph-API-OnlineMeeting/"/>
<category term="Graph API Event" scheme="https://rainmakerho.github.io/tags/Graph-API-Event/"/>
<category term="取得 Teams 逐字稿" scheme="https://rainmakerho.github.io/tags/%E5%8F%96%E5%BE%97-Teams-%E9%80%90%E5%AD%97%E7%A8%BF/"/>
<category term="No application access policy found for this app" scheme="https://rainmakerho.github.io/tags/No-application-access-policy-found-for-this-app/"/>
</entry>
<entry>
<title>GPT-4.1 與 GPT-5 API 價格比較:成本差異與使用情境解析</title>
<link href="https://rainmakerho.github.io/2025/08/11/gpt4-1-vs-gpt5-api-pricing-comparison/"/>
<id>https://rainmakerho.github.io/2025/08/11/gpt4-1-vs-gpt5-api-pricing-comparison/</id>
<published>2025-08-11T02:37:22.000Z</published>
<updated>2025-08-11T02:41:37.613Z</updated>
<content type="html"><![CDATA[<p>隨著 OpenAI 推出 GPT-5,開發者在選擇模型時除了性能外,價格也是關鍵考量因素。本文將針對 <strong>GPT-4.1</strong> 與 <strong>GPT-5</strong> 的 API 計價方式,進行詳細對照與分析,幫助你在不同應用場景下作出最佳選擇。</p><hr><h2 id="1-價格對照表(每百萬-tokens-x2F-美元)"><a href="#1-價格對照表(每百萬-tokens-x2F-美元)" class="headerlink" title="1. 價格對照表(每百萬 tokens/美元)"></a>1. 價格對照表(每百萬 tokens/美元)</h2><table><thead><tr><th>模型</th><th>Input(輸入)</th><th>Cached Input(快取輸入)</th><th>Output(輸出)</th></tr></thead><tbody><tr><td><strong>GPT-4.1</strong></td><td>$2.00</td><td>$0.50</td><td>$8.00</td></tr><tr><td><strong>GPT-5</strong></td><td>$1.25</td><td>$0.125</td><td>$10.00</td></tr></tbody></table><blockquote><p>註:Cached Input 是指重複使用的輸入 tokens,計價更低,適合多輪對話或相似請求。</p></blockquote><hr><h2 id="2-價格差異分析"><a href="#2-價格差異分析" class="headerlink" title="2. 價格差異分析"></a>2. 價格差異分析</h2><h3 id="2-1-輸入成本"><a href="#2-1-輸入成本" class="headerlink" title="2.1 輸入成本"></a>2.1 輸入成本</h3><ul><li>GPT-5 輸入價格 <strong>比 GPT-4.1 便宜 37.5%($1.25 vs $2.00)</strong>。</li><li>Cached Input 成本更大幅下降到 GPT-4.1 的 <strong>1/4</strong>($0.125 vs $0.50)。</li></ul><h3 id="2-2-輸出成本"><a href="#2-2-輸出成本" class="headerlink" title="2.2 輸出成本"></a>2.2 輸出成本</h3><ul><li>GPT-5 輸出價格 <strong>比 GPT-4.1 高 25%($10.00 vs $8.00)</strong>。</li><li>在長輸出的情境下,GPT-4.1 可能更具成本優勢。</li></ul><hr><h2 id="3-使用情境建議"><a href="#3-使用情境建議" class="headerlink" title="3. 使用情境建議"></a>3. 使用情境建議</h2><table><thead><tr><th>使用情境</th><th>建議選擇</th><th>理由</th></tr></thead><tbody><tr><td><strong>長 prompt + 短輸出</strong></td><td>GPT-5</td><td>輸入便宜,總成本低</td></tr><tr><td><strong>短 prompt + 長輸出</strong></td><td>GPT-4.1</td><td>輸出便宜,適合生成大量文字</td></tr><tr><td><strong>多輪對話、快取重用多</strong></td><td>GPT-5</td><td>Cached Input 成本極低</td></tr></tbody></table><hr><h2 id="4-結論"><a href="#4-結論" class="headerlink" title="4. 結論"></a>4. 結論</h2><ul><li>如果你的應用場景 <strong>輸入量大、輸出量小</strong>,GPT-5 的成本優勢明顯。</li><li>如果你的應用場景 <strong>輸出文字長</strong>,GPT-4.1 可能更划算。</li><li>建議根據實際 token 使用比例,計算預估費用再決定模型選擇。</li></ul><hr><p><strong>延伸閱讀:</strong></p><ul><li><a href="https://platform.openai.com/docs/models/gpt-4.1">OpenAI GPT-4.1 官方文件</a></li><li><a href="https://platform.openai.com/docs/models/gpt-5">OpenAI GPT-5 官方文件</a></li></ul>]]></content>
<summary type="html"><p>隨著 OpenAI 推出 GPT-5,開發者在選擇模型時除了性能外,價格也是關鍵考量因素。本文將針對 <strong>GPT-4.1</strong> 與 <strong>GPT-5</strong> 的 API 計價方式,進行詳細對照與分析,幫助你在不同應用場景下作出最佳</summary>
<category term="OpenAI" scheme="https://rainmakerho.github.io/tags/OpenAI/"/>
<category term="GPT-4.1" scheme="https://rainmakerho.github.io/tags/GPT-4-1/"/>
<category term="GPT-5" scheme="https://rainmakerho.github.io/tags/GPT-5/"/>
<category term="API pricing" scheme="https://rainmakerho.github.io/tags/API-pricing/"/>
<category term="token cost" scheme="https://rainmakerho.github.io/tags/token-cost/"/>
<category term="AI model comparison" scheme="https://rainmakerho.github.io/tags/AI-model-comparison/"/>
<category term="GPT-4.1 price" scheme="https://rainmakerho.github.io/tags/GPT-4-1-price/"/>
<category term="GPT-5 price" scheme="https://rainmakerho.github.io/tags/GPT-5-price/"/>
<category term="cached input" scheme="https://rainmakerho.github.io/tags/cached-input/"/>
<category term="input token cost" scheme="https://rainmakerho.github.io/tags/input-token-cost/"/>
<category term="output token cost" scheme="https://rainmakerho.github.io/tags/output-token-cost/"/>
<category term="GPT-4.1 vs GPT-5" scheme="https://rainmakerho.github.io/tags/GPT-4-1-vs-GPT-5/"/>
<category term="AI cost optimization" scheme="https://rainmakerho.github.io/tags/AI-cost-optimization/"/>
<category term="GPT API" scheme="https://rainmakerho.github.io/tags/GPT-API/"/>
<category term="AI pricing guide" scheme="https://rainmakerho.github.io/tags/AI-pricing-guide/"/>
</entry>
<entry>
<title>C# 用 Enum + Dictionary 實作 狀態機:以會員狀態轉換為例</title>
<link href="https://rainmakerho.github.io/2025/08/06/csharp-state-machine-enum-dictionary/"/>
<id>https://rainmakerho.github.io/2025/08/06/csharp-state-machine-enum-dictionary/</id>
<published>2025-08-06T09:30:35.000Z</published>
<updated>2025-08-06T09:43:52.450Z</updated>
<content type="html"><![CDATA[<p>在軟體開發中,有限狀態機(Finite State Machine, FSM) 是處理狀態之間有明確規則的轉換時非常常用的設計。例如,會員從「入會」可以轉為「暫停」或「退會」,「退會」又可以重新「入會」等。若直接用 if-else 判斷,不僅難以維護,日後擴充更易出錯。本文將介紹如何利用 C# 的 Enum 搭配 Dictionary,簡潔又彈性地實作狀態轉換邏輯。</p><h3 id="實作練習"><a href="#實作練習" class="headerlink" title="實作練習"></a>實作練習</h3><h5 id="1-用-Enum-定義所有狀態"><a href="#1-用-Enum-定義所有狀態" class="headerlink" title="1.用 Enum 定義所有狀態"></a>1.用 Enum 定義所有狀態</h5><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="built_in">enum</span> MemberStatus</span><br><span class="line">{</span><br><span class="line"> 入會,</span><br><span class="line"> 暫停,</span><br><span class="line"> 退會</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h5 id="2-用-Dictionary-定義合法狀態轉換"><a href="#2-用-Dictionary-定義合法狀態轉換" class="headerlink" title="2.用 Dictionary 定義合法狀態轉換"></a>2.用 Dictionary 定義合法狀態轉換</h5><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title">MemberStatusExtensions</span></span><br><span class="line">{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">readonly</span> Dictionary<MemberStatus, List<MemberStatus>> AllowedTransitions = <span class="keyword">new</span>()</span><br><span class="line"> {</span><br><span class="line"> [<span class="meta">MemberStatus.入會</span>] = [MemberStatus.暫停, MemberStatus.退會],</span><br><span class="line"> [<span class="meta">MemberStatus.暫停</span>] = [MemberStatus.入會, MemberStatus.退會],</span><br><span class="line"> [<span class="meta">MemberStatus.退會</span>] = [MemberStatus.入會]</span><br><span class="line"> };</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="built_in">bool</span> <span class="title">CanTransitionTo</span>(<span class="params"><span class="keyword">this</span> MemberStatus current, MemberStatus target</span>)</span></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">return</span> AllowedTransitions.TryGetValue(current, <span class="keyword">out</span> <span class="keyword">var</span> nexts) && nexts.Contains(target);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h5 id="3-使用"><a href="#3-使用" class="headerlink" title="3.使用"></a>3.使用</h5><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> current = MemberStatus.入會;</span><br><span class="line"><span class="keyword">var</span> target = MemberStatus.退會;</span><br><span class="line"><span class="keyword">if</span> (current.CanTransitionTo(target))</span><br><span class="line">{</span><br><span class="line"> Console.WriteLine(<span class="string">"允許轉換"</span>);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">{</span><br><span class="line"> Console.WriteLine(<span class="string">"不允許轉換"</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="結論"><a href="#結論" class="headerlink" title="結論"></a>結論</h3><p>這種設計方式具有以下優點:</p><ul><li>可讀性高:狀態與轉換規則集中管理,一目瞭然。</li><li>易於擴充:日後如需新增狀態,只需修改 Enum 與 Dictionary 即可。</li><li>易於維護:轉換規則變更時,不需翻找大量 if-else,只需調整字典內容。</li></ul>]]></content>
<summary type="html"><p>在軟體開發中,有限狀態機(Finite State Machine, FSM) 是處理狀態之間有明確規則的轉換時非常常用的設計。例如,會員從「入會」可以轉為「暫停」或「退會」,「退會」又可以重新「入會」等。若直接用 if-else 判斷,不僅難以維護,日後擴充更易出錯。本文</summary>
<category term="C#" scheme="https://rainmakerho.github.io/tags/C/"/>
<category term="Dictionary" scheme="https://rainmakerho.github.io/tags/Dictionary/"/>
<category term="Finite State Machine" scheme="https://rainmakerho.github.io/tags/Finite-State-Machine/"/>
<category term="Enum" scheme="https://rainmakerho.github.io/tags/Enum/"/>
<category term="狀態轉換" scheme="https://rainmakerho.github.io/tags/%E7%8B%80%E6%85%8B%E8%BD%89%E6%8F%9B/"/>
<category term="會員管理" scheme="https://rainmakerho.github.io/tags/%E6%9C%83%E5%93%A1%E7%AE%A1%E7%90%86/"/>
</entry>
<entry>
<title>GPT 給 Image Base64 字串花費的 Token數比給 Url 還來得多很多?</title>
<link href="https://rainmakerho.github.io/2025/07/31/gpt-image-token-calculation-url-vs-base64/"/>
<id>https://rainmakerho.github.io/2025/07/31/gpt-image-token-calculation-url-vs-base64/</id>
<published>2025-07-31T08:12:08.000Z</published>
<updated>2025-08-01T02:45:09.716Z</updated>
<content type="html"><![CDATA[<h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>有人說使用 gpt-4o, gpt-4.1 這種多模態 LLM,呼叫 ChatCompletion API 給 Image 時,<br>如果給 Image 的 Base64 內容,所花的 Token 數會比給 Image URL 來得多很多 (´− `) ンー (¬_¬)<br>是因為 Image 的 Base64 字串長度比 Image URL 的字串內容多很多。</p><p>所以,如果要給圖檔時,要想儘辦法讓 OpenAI API 可以讀取到圖檔,<br>也就是要允許圖檔可以讓 internet 連到 !!!</p><blockquote><p>Image 的 Base64 內容,所花的 Token 數會比給 Image URL 來得多很多,這是真的嗎?</p></blockquote><p>以下我們就來驗證看看,</p><h3 id="測試"><a href="#測試" class="headerlink" title="測試"></a>測試</h3><p>使用 Semantic Kernel C#,使用 <code>ImageContent</code> 分別給 url 及 file bytes (base64),程式如下,</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br></pre></td><td class="code"><pre><span class="line">IKernelBuilder builder = Kernel.CreateBuilder();</span><br><span class="line"><span class="keyword">const</span> <span class="built_in">string</span> apikey = <span class="string">"sk-請給 openai apikey"</span>;</span><br><span class="line"><span class="keyword">const</span> <span class="built_in">string</span> model = <span class="string">"gpt-4.1-mini"</span>;</span><br><span class="line"></span><br><span class="line">builder.AddOpenAIChatCompletion(model, apikey);</span><br><span class="line">Kernel kernel = builder.Build();</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();</span><br><span class="line"></span><br><span class="line">ChatHistory chatHistory = <span class="keyword">new</span>();</span><br><span class="line"><span class="built_in">string</span> textContent = <span class="string">"請將摘要這張圖片中的文字。\r\n"</span>;</span><br><span class="line"><span class="built_in">bool</span> isUseUri = <span class="literal">true</span>; <span class="comment">//or false</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (isUseUri)</span><br><span class="line">{</span><br><span class="line"> chatHistory.Add(</span><br><span class="line"> <span class="keyword">new</span>()</span><br><span class="line"> {</span><br><span class="line"> Role = AuthorRole.User,</span><br><span class="line"> Items = [</span><br><span class="line"> <span class="keyword">new</span> TextContent(textContent),</span><br><span class="line"> <span class="keyword">new</span> ImageContent(<span class="keyword">new</span> Uri(<span class="string">$"<span class="subst">{對外的ImageUrl}</span>"</span>))</span><br><span class="line"> ]</span><br><span class="line"> }</span><br><span class="line"> );</span><br><span class="line">}</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">{</span><br><span class="line"> <span class="built_in">byte</span>[] imageBytes = File.ReadAllBytes(<span class="string">"path/to/your/image.png"</span>);</span><br><span class="line"> chatHistory.Add(</span><br><span class="line"> <span class="keyword">new</span>()</span><br><span class="line"> {</span><br><span class="line"> Role = AuthorRole.User,</span><br><span class="line"> Items = [</span><br><span class="line"> <span class="keyword">new</span> TextContent(textContent),</span><br><span class="line"> <span class="keyword">new</span> ImageContent(imageBytes, <span class="string">"image/png"</span>)</span><br><span class="line"> ]</span><br><span class="line"> }</span><br><span class="line"> );</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> reply = <span class="keyword">await</span> chatCompletionService.GetChatMessageContentAsync(chatHistory);</span><br><span class="line">Console.WriteLine(<span class="string">"================"</span>);</span><br><span class="line">Console.WriteLine(reply.Content);</span><br><span class="line">Console.WriteLine(<span class="string">"================"</span>);</span><br><span class="line">Helper.OutputInnerContent(reply.InnerContent <span class="keyword">as</span> OpenAI.Chat.ChatCompletion);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">OutputInnerContent</span>(<span class="params">OpenAI.Chat.ChatCompletion innerContent</span>)</span></span><br><span class="line">{</span><br><span class="line"> Console.WriteLine(<span class="string">$"Message role: <span class="subst">{innerContent.Role}</span>"</span>); <span class="comment">// Available as a property of ChatMessageContent</span></span><br><span class="line"> Console.WriteLine(<span class="string">$"Message content: <span class="subst">{innerContent.Content[<span class="number">0</span>].Text}</span>"</span>); <span class="comment">// Available as a property of ChatMessageContent</span></span><br><span class="line"></span><br><span class="line"> Console.WriteLine(<span class="string">$"Model: <span class="subst">{innerContent.Model}</span>"</span>); <span class="comment">// Model doesn't change per chunk, so we can get it from the first chunk only</span></span><br><span class="line"> Console.WriteLine(<span class="string">$"Created At: <span class="subst">{innerContent.CreatedAt}</span>"</span>);</span><br><span class="line"></span><br><span class="line"> Console.WriteLine(<span class="string">$"Finish reason: <span class="subst">{innerContent.FinishReason}</span>"</span>);</span><br><span class="line"> Console.WriteLine(<span class="string">$"Input tokens usage: <span class="subst">{innerContent.Usage.InputTokenCount}</span>"</span>);</span><br><span class="line"> Console.WriteLine(<span class="string">$"Output tokens usage: <span class="subst">{innerContent.Usage.OutputTokenCount}</span>"</span>);</span><br><span class="line"> Console.WriteLine(<span class="string">$"Total tokens usage: <span class="subst">{innerContent.Usage.TotalTokenCount}</span>"</span>);</span><br><span class="line"> Console.WriteLine(<span class="string">$"Refusal: <span class="subst">{innerContent.Refusal}</span> "</span>);</span><br><span class="line"> Console.WriteLine(<span class="string">$"Id: <span class="subst">{innerContent.Id}</span>"</span>);</span><br><span class="line"> Console.WriteLine(<span class="string">$"System fingerprint: <span class="subst">{innerContent.SystemFingerprint}</span>"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (innerContent.ContentTokenLogProbabilities.Count > <span class="number">0</span>)</span><br><span class="line"> {</span><br><span class="line"> Console.WriteLine(<span class="string">"Content token log probabilities:"</span>);</span><br><span class="line"> <span class="keyword">foreach</span> (<span class="keyword">var</span> contentTokenLogProbability <span class="keyword">in</span> innerContent.ContentTokenLogProbabilities)</span><br><span class="line"> {</span><br><span class="line"> Console.WriteLine(<span class="string">$"Token: <span class="subst">{contentTokenLogProbability.Token}</span>"</span>);</span><br><span class="line"> Console.WriteLine(<span class="string">$"Log probability: <span class="subst">{contentTokenLogProbability.LogProbability}</span>"</span>);</span><br><span class="line"></span><br><span class="line"> Console.WriteLine(<span class="string">" Top log probabilities for this token:"</span>);</span><br><span class="line"> <span class="keyword">foreach</span> (<span class="keyword">var</span> topLogProbability <span class="keyword">in</span> contentTokenLogProbability.TopLogProbabilities)</span><br><span class="line"> {</span><br><span class="line"> Console.WriteLine(<span class="string">$" Token: <span class="subst">{topLogProbability.Token}</span>"</span>);</span><br><span class="line"> Console.WriteLine(<span class="string">$" Log probability: <span class="subst">{topLogProbability.LogProbability}</span>"</span>);</span><br><span class="line"> Console.WriteLine(<span class="string">" ======="</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> Console.WriteLine(<span class="string">"--------------"</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (innerContent.RefusalTokenLogProbabilities.Count > <span class="number">0</span>)</span><br><span class="line"> {</span><br><span class="line"> Console.WriteLine(<span class="string">"Refusal token log probabilities:"</span>);</span><br><span class="line"> <span class="keyword">foreach</span> (<span class="keyword">var</span> refusalTokenLogProbability <span class="keyword">in</span> innerContent.RefusalTokenLogProbabilities)</span><br><span class="line"> {</span><br><span class="line"> Console.WriteLine(<span class="string">$"Token: <span class="subst">{refusalTokenLogProbability.Token}</span>"</span>);</span><br><span class="line"> Console.WriteLine(<span class="string">$"Log probability: <span class="subst">{refusalTokenLogProbability.LogProbability}</span>"</span>);</span><br><span class="line"></span><br><span class="line"> Console.WriteLine(<span class="string">" Refusal top log probabilities for this token:"</span>);</span><br><span class="line"> <span class="keyword">foreach</span> (<span class="keyword">var</span> topLogProbability <span class="keyword">in</span> refusalTokenLogProbability.TopLogProbabilities)</span><br><span class="line"> {</span><br><span class="line"> Console.WriteLine(<span class="string">$" Token: <span class="subst">{topLogProbability.Token}</span>"</span>);</span><br><span class="line"> Console.WriteLine(<span class="string">$" Log probability: <span class="subst">{topLogProbability.LogProbability}</span>"</span>);</span><br><span class="line"> Console.WriteLine(<span class="string">" ======="</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><ul><li>註: <code>new ImageContent(imageBytes, "image/png")</code>中的 <code>imageBytes</code>會被轉成 Base64 字串(<a href="https://github.com/openai/openai-dotnet/blob/main/src/Utility/DataEncodingHelpers.cs#L37">DataEncodingHelpers.cs</a>),如下程式,</li></ul><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="built_in">string</span> <span class="title">CreateDataUri</span>(<span class="params">BinaryData bytes, <span class="built_in">string</span> bytesMediaType</span>)</span></span><br><span class="line">{</span><br><span class="line"> <span class="built_in">string</span> base64Bytes = Convert.ToBase64String(bytes.ToArray());</span><br><span class="line"> <span class="keyword">return</span> <span class="string">$"data:<span class="subst">{bytesMediaType}</span>;base64,<span class="subst">{base64Bytes}</span>"</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>程式 Log 出來的輸入 Token 是 2,456 個,如下圖:</p><img src="/2025/07/31/gpt-image-token-calculation-url-vs-base64/02.png" class="" title="AP Log"><p>從 OpenAI 的 Log 來看,2 次的 Input Token 都是 2,456 個,跟我們程式 Log 出來的結果相同,如下圖:</p><img src="/2025/07/31/gpt-image-token-calculation-url-vs-base64/01.png" class="" title="OpenAI Log"><h3 id="總結"><a href="#總結" class="headerlink" title="總結"></a>總結</h3><p>使用多模態 LLM,給 Image 的 Url 或是給 Base64 字串,所花費的 Input Token 數是<strong>一樣的</strong>!<br>差別就在於 Post API 時的 Payload 大小而已。<br>如果是企業內的圖檔,建議使用 Base64 的方式,也不會有圖檔要對外的問題。</p><p>最後,再強調一次,</p><blockquote><p>使用多模態 LLM,給 Image 的 Url 或是給 Base64 字串,所花費的 Input Token 數是<strong>一樣的</strong> (>人<)</p></blockquote><h3 id="參考資源"><a href="#參考資源" class="headerlink" title="參考資源"></a>參考資源</h3><p><a href="https://learn.microsoft.com/en-us/semantic-kernel/concepts/ai-services/chat-completion/multi-modal-chat-completion?pivots=programming-language-csharp">Multi-modal chat completion</a><br><a href="https://github.com/openai/openai-dotnet/blob/main/src/Utility/DataEncodingHelpers.cs#L37">DataEncodingHelpers.cs - CreateDataUri</a></p>]]></content>
<summary type="html"><h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>有人說使用 gpt-4o, gpt-4.1 這種多模態 LLM,呼叫 ChatCompletion API 給 Image 時,<br>如果</summary>
<category term="C#" scheme="https://rainmakerho.github.io/tags/C/"/>
<category term="Base64" scheme="https://rainmakerho.github.io/tags/Base64/"/>
<category term="Semantic Kernel" scheme="https://rainmakerho.github.io/tags/Semantic-Kernel/"/>
<category term="URL" scheme="https://rainmakerho.github.io/tags/URL/"/>
<category term="Image" scheme="https://rainmakerho.github.io/tags/Image/"/>
<category term="GPT" scheme="https://rainmakerho.github.io/tags/GPT/"/>
<category term="token 計費" scheme="https://rainmakerho.github.io/tags/token-%E8%A8%88%E8%B2%BB/"/>
<category term="API token usage" scheme="https://rainmakerho.github.io/tags/API-token-usage/"/>
<category term="ChatCompletion" scheme="https://rainmakerho.github.io/tags/ChatCompletion/"/>
</entry>
<entry>
<title>當 Google Maps Static API Polyline 點數過多:錯誤原因與最佳解法</title>
<link href="https://rainmakerho.github.io/2025/07/31/google-map-polyline-too-many-points-staticmap-error-solution/"/>
<id>https://rainmakerho.github.io/2025/07/31/google-map-polyline-too-many-points-staticmap-error-solution/</id>
<published>2025-07-31T01:55:51.000Z</published>
<updated>2025-08-07T02:45:05.255Z</updated>
<content type="html"><![CDATA[<h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>最近使用 C# 透過<a href="https://developers.google.cn/maps/documentation/routes/reference/rest/v2/TopLevel/computeRoutes?hl=zh-tw">Google Map Routes API 的 Compute Routes</a> 來規劃起、迄的路徑,取得<strong>polyline</strong>(整體路線的折線),再利用<a href="https://developers.google.com/maps/documentation/maps-static/overview?hl=zh-tw">Google Maps Static API</a>來產生含有起、迄路徑的圖。<br>但是如果起、迄路徑中的折線太多,例如使用<strong>開車</strong>的方式,從<strong>上海</strong>到<strong>深圳</strong>,它的折線就有<strong>8 千多個</strong>,這就會導致使用<a href="https://developers.google.com/maps/documentation/maps-static/overview?hl=zh-tw">Google Maps Static API</a>它的<strong>URL</strong>長度會爆長而導致發生以下的錯誤,</p><blockquote><p>Your client has issued a malformed or illegal request. That’s all we know.</p></blockquote><h3 id="解決方法"><a href="#解決方法" class="headerlink" title="解決方法"></a>解決方法</h3><p><a href="https://developers.google.com/maps/documentation/maps-static/start?hl=zh-tw#url-size-restriction">Maps Static API 網址長度上限為 16384 個字元</a>,所以我們要做的就是要<strong>減少</strong>折線的數量。</p><h5 id="安裝-Nuget-套件"><a href="#安裝-Nuget-套件" class="headerlink" title="安裝 Nuget 套件"></a>安裝 Nuget 套件</h5><p><a href="https://www.nuget.org/packages/nettopologysuite/">NetTopologySuite</a>:減少 經緯度的點數<br><a href="https://www.nuget.org/packages/Polyliner.Net">Polyliner.Net</a>:Encode 經緯度 成 polyline, 將 polyline Decode 成 經緯度</p><h5 id="處理步驟-及-程式碼"><a href="#處理步驟-及-程式碼" class="headerlink" title="處理步驟 及 程式碼"></a>處理步驟 及 程式碼</h5><p>1.將<strong>polyline</strong> Decode 取得經緯度</p><p>2.利用線段簡化演算法,減少 經緯度 的數量(要減少到多少,可以自定一個<code>SimplifyToTargetPoints</code> Method 來處理它)</p><p>3.重新將經緯度 Encode 成 <strong>polyline</strong></p><p>程式碼如下,</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> NetTopologySuite.Geometries;</span><br><span class="line"><span class="keyword">using</span> PolylinerNet;</span><br><span class="line"><span class="keyword">using</span> NetTopologySuite.Simplify;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> encodedPoyline = <span class="string">"Compute Routes 取回的 encodedPolyline"</span>;</span><br><span class="line"><span class="keyword">var</span> polyliner = <span class="keyword">new</span> Polyliner();</span><br><span class="line"><span class="comment">// 1.將 polyline Decode 取得經緯度</span></span><br><span class="line">List<PolylinePoint> polylinePointList = polyliner.Decode(encodedPoyline);</span><br><span class="line"><span class="keyword">var</span> coordinates = polylinePointList</span><br><span class="line"> .Select(p => <span class="keyword">new</span> Coordinate(p.Longitude, p.Latitude))</span><br><span class="line"> .ToArray();</span><br><span class="line"><span class="comment">// 2.利用線段簡化演算法,減少 經緯度 的數量</span></span><br><span class="line"><span class="keyword">var</span> geometryFactory = <span class="keyword">new</span> GeometryFactory();</span><br><span class="line"><span class="keyword">var</span> line = geometryFactory.CreateLineString(coordinates);</span><br><span class="line"><span class="comment">// 減少 經緯度 的數量到 300 以內,請依需求進行調整</span></span><br><span class="line"><span class="keyword">var</span> simplifiedLine = SimplifyToTargetPoints(line, <span class="number">300</span>);</span><br><span class="line"><span class="keyword">var</span> simplifiedPoints = simplifiedLine.Coordinates</span><br><span class="line"> .Select(c => <span class="keyword">new</span> PolylinePoint(c.Y, c.X))</span><br><span class="line"> .ToList();</span><br><span class="line"><span class="comment">// 3.新將經緯度 Encode 成 polyline</span></span><br><span class="line"><span class="keyword">var</span> encodedSimplifiedPolyline = polyliner.Encode(simplifiedPoints);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 減少 經緯度 的數量到 targetPoints 以內</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> LineString <span class="title">SimplifyToTargetPoints</span>(<span class="params">LineString line, <span class="built_in">int</span> targetPoints, <span class="built_in">double</span> minTolerance = <span class="number">1e-6</span>, <span class="built_in">double</span> maxTolerance = <span class="number">1.0</span>, <span class="built_in">int</span> maxIterations = <span class="number">20</span></span>)</span></span><br><span class="line">{</span><br><span class="line"> <span class="keyword">if</span> (line.NumPoints <= targetPoints)</span><br><span class="line"> <span class="keyword">return</span> line; <span class="comment">// 原本就很少</span></span><br><span class="line"></span><br><span class="line"> <span class="built_in">double</span> low = minTolerance;</span><br><span class="line"> <span class="built_in">double</span> high = maxTolerance;</span><br><span class="line"> LineString result = line;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> (<span class="built_in">int</span> i = <span class="number">0</span>; i < maxIterations; i++)</span><br><span class="line"> {</span><br><span class="line"> <span class="built_in">double</span> mid = (low + high) / <span class="number">2</span>;</span><br><span class="line"> <span class="keyword">var</span> simplified = DouglasPeuckerSimplifier.Simplify(line, mid) <span class="keyword">as</span> LineString;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (simplified.NumPoints > targetPoints)</span><br><span class="line"> {</span><br><span class="line"> low = mid; <span class="comment">// 容忍度不夠大,點太多,繼續增加</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> {</span><br><span class="line"> result = simplified; <span class="comment">// 有達到條件,記下這個結果</span></span><br><span class="line"> high = mid; <span class="comment">// 嘗試更小的 tolerance</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (Math.Abs(high - low) < <span class="number">1e-8</span>)</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> result;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>最後再將 encodedSimplifiedPolyline 組成 <a href="https://developers.google.com/maps/documentation/maps-static/overview?hl=zh-tw">Google Maps Static API</a> 的 URL,即可正常產生地圖。如下圖:</p><img src="/2025/07/31/google-map-polyline-too-many-points-staticmap-error-solution/01.png" class="" title="Google Map"><ul><li>註: 以 800 x 600 的圖,使用 300 以內的圖來看是還 OK, 大家也可以試看看其他的數量,例如 100, 200 …,在 <a href="https://developers.google.com/maps/documentation/routes/polylinedecoder">Polyline decoder utility</a>試看看效果</li></ul><h5 id="路徑簡化原理說明"><a href="#路徑簡化原理說明" class="headerlink" title="路徑簡化原理說明"></a>路徑簡化原理說明</h5><p>通常使用 <a href="https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm">Ramer–Douglas–Peucker algorithm</a>,如 wikipedia 圖所示:<br><img src="https://upload.wikimedia.org/wikipedia/commons/3/30/Douglas-Peucker_animated.gif"></p><h3 id="參考資源"><a href="#參考資源" class="headerlink" title="參考資源"></a>參考資源</h3><p><a href="https://developers.google.cn/maps/documentation/routes/reference/rest/v2/TopLevel/computeRoutes?hl=zh-tw">Google Map Routes API 的 Compute Routes</a><br><a href="https://developers.google.com/maps/documentation/maps-static/overview?hl=zh-tw">Google Maps Static API</a><br><a href="https://developers.google.com/maps/documentation/maps-static/start?hl=zh-tw#url-size-restriction">Maps Static API 網址長度上限為 16384 個字元</a><br><a href="https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm">Ramer–Douglas–Peucker algorithm</a><br><a href="https://developers.google.com/maps/documentation/routes/polylinedecoder">Polyline decoder utility</a></p>]]></content>
<summary type="html"><h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>最近使用 C# 透過<a href="https://developers.google.cn/maps/documentation/rou</summary>
<category term="C#" scheme="https://rainmakerho.github.io/tags/C/"/>
<category term="URL" scheme="https://rainmakerho.github.io/tags/URL/"/>
<category term="Polyline" scheme="https://rainmakerho.github.io/tags/Polyline/"/>
<category term="Google Maps API" scheme="https://rainmakerho.github.io/tags/Google-Maps-API/"/>
<category term="Maps Static" scheme="https://rainmakerho.github.io/tags/Maps-Static/"/>
<category term="點數過多" scheme="https://rainmakerho.github.io/tags/%E9%BB%9E%E6%95%B8%E9%81%8E%E5%A4%9A/"/>
<category term="NetTopologySuite" scheme="https://rainmakerho.github.io/tags/NetTopologySuite/"/>
<category term="簡化路徑" scheme="https://rainmakerho.github.io/tags/%E7%B0%A1%E5%8C%96%E8%B7%AF%E5%BE%91/"/>
<category term="Your client has issued a malformed or illegal request" scheme="https://rainmakerho.github.io/tags/Your-client-has-issued-a-malformed-or-illegal-request/"/>
<category term="DouglasPeuckerSimplifier" scheme="https://rainmakerho.github.io/tags/DouglasPeuckerSimplifier/"/>
</entry>
<entry>
<title>IIS Application Pool 檔案目錄權限解析:為什麼不用加帳號也能運作?</title>
<link href="https://rainmakerho.github.io/2025/07/29/iis-application-pool-identity-directory-permissions/"/>
<id>https://rainmakerho.github.io/2025/07/29/iis-application-pool-identity-directory-permissions/</id>
<published>2025-07-29T03:31:56.000Z</published>
<updated>2025-07-29T06:41:42.247Z</updated>
<content type="html"><![CDATA[<h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>在將 ASP.NET Core 網站部署到 IIS 的過程中,許多人都遇過這個疑問:</p><p>1.部署網站目錄時,是否一定要手動加上 <code>IIS AppPool\YourAppPool</code> 帳號的權限?</p><p>2.為什麼很多時候沒加也能正常執行?</p><p>這篇文章將徹底解析 IIS Application Pool Identity 的目錄權限運作原理,分享實際測試、設定流程,以及安全性最佳實踐,幫助你徹底搞懂這個主題!</p><h3 id="IIS-Application-Pool-Identity"><a href="#IIS-Application-Pool-Identity" class="headerlink" title="IIS Application Pool Identity"></a>IIS Application Pool Identity</h3><p>IIS 預設會使用 Application Pool Identity(<code>IIS AppPool\YourAppPool</code>)來執行網站程序。它有以下特點:</p><ul><li>只要目錄權限有 <code>BUILTIN\Users</code> 或 <code>NT AUTHORITY\Authenticated Users</code>,IIS AppPool Identity 預設就能正常存取。</li><li>如果這些群組被移除,就必須**明確加入 <code>IIS AppPool\YourAppPool</code>**,否則會遇到 500 錯誤。</li></ul><p>這樣的設計,雖然方便,但不符合「最小權限原則」(Least Privilege Principle)。<br>建議為生產環境<strong>明確只授權給實際需要的 AppPool Identity,移除其他不必要的群組帳號</strong>。</p><h3 id="ASP-NET-Core-網站在-IIS-部署與最小權限設定"><a href="#ASP-NET-Core-網站在-IIS-部署與最小權限設定" class="headerlink" title="ASP.NET Core 網站在 IIS 部署與最小權限設定"></a>ASP.NET Core 網站在 IIS 部署與最小權限設定</h3><p>以下是一套完整、符合最佳實踐的 IIS 目錄權限設定步驟:</p><p>1.在部署的 Server 上安裝 ASP.NET Core Hosting Bundle 後,執行 <code>iisreset</code></p><p>2.在 IIS 中建立一個新的<strong>應用程式集區</strong>(假設名稱為<code>webdemo</code>),如下圖:</p><img src="/2025/07/29/iis-application-pool-identity-directory-permissions/01.png" class="" title="new application_pool"><p>3.在 IIS 中建立一個新的<strong>應用程式</strong>(假設名稱為<code>webdemo</code>),並將應用程式集區指定到上面建立的集區,設定檔案所在目錄</p><img src="/2025/07/29/iis-application-pool-identity-directory-permissions/02.png" class="" title="new application"><p>4.在 IIS 站台,點擊<code>webdemo</code>,設定<strong>HTTP 回應標頭</strong>(移除不必要的標頭,加入 Security 的一些標頭),如下圖:</p><img src="/2025/07/29/iis-application-pool-identity-directory-permissions/06.png" class="" title="http headers"><p>5.在檔案總管中,點選部署目錄->內容->安全性->SYSTEM 的權限(P)->點擊「進階」Button->點擊「停用繼承(I)」 Button 後,按下「確定」,如下圖:</p><img src="/2025/07/29/iis-application-pool-identity-directory-permissions/04.png" class="" title="SYSTEM"><p>6.在檔案總管中,點選部署目錄->內容->安全性->群組或使用者名稱(G)->點擊「編輯」Button->移除<strong>Authenticated Users</strong> 及 <strong>Users</strong> 群組帳號,如下圖:</p><img src="/2025/07/29/iis-application-pool-identity-directory-permissions/03.png" class="" title="remove users"><p>7.在檔案總管中,點選部署目錄->內容->安全性->群組或使用者名稱(G)->點擊「編輯」Button->加入 <strong>IIS AppPool\webdemo</strong> 帳號(允許<code>讀取與執行</code>, <code>列出資料夾內容</code> 及 <code>讀取</code> 權限),如下圖:</p><img src="/2025/07/29/iis-application-pool-identity-directory-permissions/05.png" class="" title="add appPool"><br/><img src="/2025/07/29/iis-application-pool-identity-directory-permissions/07.png" class="" title="set appPool permissions"><p>8.針對特定子目錄(如 uploads、logs)加「寫入」或「寫入 + 修改」(有要刪除檔案) 權限,如下圖:</p><img src="/2025/07/29/iis-application-pool-identity-directory-permissions/08.png" class="" title="set uploads permissions"><h3 id="參考資源"><a href="#參考資源" class="headerlink" title="參考資源"></a>參考資源</h3><p><a href="https://learn.microsoft.com/zh-tw/iis/manage/configuring-security/application-pool-identities">應用程式集區身分識別</a></p>]]></content>
<summary type="html"><h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>在將 ASP.NET Core 網站部署到 IIS 的過程中,許多人都遇過這個疑問:</p>
<p>1.部署網站目錄時,是否一定要手動加上 </summary>
<category term="IIS" scheme="https://rainmakerho.github.io/tags/IIS/"/>
<category term="Application Pool" scheme="https://rainmakerho.github.io/tags/Application-Pool/"/>
<category term="ASP.NET Core" scheme="https://rainmakerho.github.io/tags/ASP-NET-Core/"/>
<category term="權限" scheme="https://rainmakerho.github.io/tags/%E6%AC%8A%E9%99%90/"/>
<category term="目錄權限" scheme="https://rainmakerho.github.io/tags/%E7%9B%AE%E9%8C%84%E6%AC%8A%E9%99%90/"/>
<category term="AppPool Identity" scheme="https://rainmakerho.github.io/tags/AppPool-Identity/"/>
<category term="虛擬帳號" scheme="https://rainmakerho.github.io/tags/%E8%99%9B%E6%93%AC%E5%B8%B3%E8%99%9F/"/>
<category term="Windows Server" scheme="https://rainmakerho.github.io/tags/Windows-Server/"/>
<category term="網站部署" scheme="https://rainmakerho.github.io/tags/%E7%B6%B2%E7%AB%99%E9%83%A8%E7%BD%B2/"/>
<category term="安全性" scheme="https://rainmakerho.github.io/tags/%E5%AE%89%E5%85%A8%E6%80%A7/"/>
<category term="最佳實踐" scheme="https://rainmakerho.github.io/tags/%E6%9C%80%E4%BD%B3%E5%AF%A6%E8%B8%90/"/>
</entry>
<entry>
<title>低成本高效率!用 Dify + Azure AI 快速處理手寫發票,準確率超高</title>
<link href="https://rainmakerho.github.io/2025/07/25/dify-handwritten-invoice/"/>
<id>https://rainmakerho.github.io/2025/07/25/dify-handwritten-invoice/</id>
<published>2025-07-25T06:18:35.000Z</published>
<updated>2025-08-07T02:33:00.408Z</updated>
<content type="html"><![CDATA[<h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>手寫發票的資料輸入是營業稅申報流程中最費時的一環,傳統人工輸入容易出錯又耗時。<br>本文將介紹,如何透過 Dify API 結合 Azure AI,把這段流程完全自動化,大幅減少人工作業時間與錯誤風險。</p><p>延續我在 <a href="https://rainmakerho.github.io/2025/06/23/dify-high-speed-rail-ticket-ocr/">使用 Dify 自動辨識高鐵票</a> 的實作經驗,<br>在 Dify 中只要加入 LLM 節點,使用 gpt-4.1-mini 模型來辨識<strong>高鐵票</strong>,又快又準確。<br>但是當我將<strong>高鐵票</strong>改成<strong>手寫發票</strong>時,有些手寫的數字就有可能會辨識錯誤,例如<code>9</code>,有可能會被辨識成<code>7</code>,<br>這時候要怎麼辦呢?</p><h3 id="解法"><a href="#解法" class="headerlink" title="解法"></a>解法</h3><p>GPT 雖然強大,但在辨識手寫內容上仍有一定限制。<br>此時我們可以改用<a href="https://learn.microsoft.com/zh-tw/azure/ai-services/content-understanding/">Azure AI Content Understanding</a>先進行辨識,再將結果交給 LLM 處理成我們需要的 JSON 格式。以下是完整流程:</p><p>1.在 Dify 中安裝 <a href="https://github.com/fujita-h/dify-plugin-azure-ai-document-intelligence">dify-plugin-azure-ai-document-intelligence</a> 插件<br>在 Dify 中,點擊<strong>外掛</strong>,點擊<strong>安裝插件</strong>,選擇<strong>Github 的</strong>,貼上<a href="https://github.com/fujita-h/dify-plugin-azure-ai-document-intelligence">dify-plugin-azure-ai-document-intelligence</a>的 url,選擇<strong>版本</strong>及<strong>套餐</strong>,再按<strong>下一個</strong>即可,如下圖:</p><img src="/2025/07/25/dify-handwritten-invoice/03.png" class="" title="dify-plugin-azure-ai-document-intelligence"><p>2.建立 Azure AI Content Understanding<br>建立好 Azure AI Foundry project 後,請點選<strong>My assets</strong>中的<strong>Models + endpoints</strong>,切到<strong>Service endpoints</strong> Tab,如下圖:</p><img src="/2025/07/25/dify-handwritten-invoice/01.png" class="" title="Azure AI Foundry project"><p>3.設定 <a href="https://github.com/fujita-h/dify-plugin-azure-ai-document-intelligence">dify-plugin-azure-ai-document-intelligence</a> 插件的授權<br>複製 Azure AI Content Understanding 的 endpoint 及 Primary Key, 設定到 <a href="https://github.com/fujita-h/dify-plugin-azure-ai-document-intelligence">dify-plugin-azure-ai-document-intelligence</a> 插件的授權,如下圖:</p><img src="/2025/07/25/dify-handwritten-invoice/02.png" class="" title="endpoint 及 Primary Key"><br/><img src="/2025/07/25/dify-handwritten-invoice/04.png" class="" title="set plugin lic"><p>4.設定流程<br>在 Dify 中,先使用<a href="https://github.com/fujita-h/dify-plugin-azure-ai-document-intelligence">dify-plugin-azure-ai-document-intelligence</a>插件,再將它的輸入結果給下個 LLM 節點來整理我們要的資訊,最後再輸出 LLM 整理後的結果,如下圖:</p><img src="/2025/07/25/dify-handwritten-invoice/05.png" class="" title="Dify"><p>插件的 Text 設定給 LLM 節點 的 上下文參數,LLM 節點的 Prompt 如下,</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">你是發票文字辨識專家,請從下列 OCR 文字中抽取統一發票資訊,並以 JSON 格式輸出以下欄位:</span><br><span class="line">{</span><br><span class="line"> "INVOICE_NO": "發票號碼,格式為開頭2碼英文字母加上8碼數字,例如 MT27721864",</span><br><span class="line"> "BUYER_UNIFORM_NO": "買方統一編號,通常出現在「買受人」或「統一編號」欄位後方",</span><br><span class="line"> "DATE": "發票日期,格式為中華民國年月日,例如 114年6月11日",</span><br><span class="line"> "SUBTOTAL": "銷售額小計(應稅金額),為未稅金額",</span><br><span class="line"> "TAX": "營業稅金額",</span><br><span class="line"> "SELLER_UNIFORM_NO": "賣方統一編號,通常出現在統一發票專用章附近",</span><br><span class="line"> "TOTAL": "總計金額,含稅"</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">請注意:</span><br><span class="line">- 所有金額與統編請輸出為半形數字</span><br><span class="line">- 日期請保留中華民國格式(例如 114年6月11日)</span><br><span class="line">- 若欄位無法明確辨識,請填入空字串 ""</span><br><span class="line">- 僅輸出純 JSON,請勿加入說明文字</span><br><span class="line"></span><br><span class="line">以下是 OCR 辨識後的文字內容:</span><br><span class="line">{{上下文內容}}</span><br></pre></td></tr></table></figure><p>完成後,拿<a href="https://digit.make9.tw/startup/entrepreneur/%E4%B8%89%E8%81%AF%E5%BC%8F%E7%99%BC%E7%A5%A8%E6%80%8E%E9%BA%BC%E9%96%8B%EF%BC%9F%E8%A4%87%E5%AF%AB%E7%B4%99%E7%94%A8%E6%B3%95%EF%BC%9F/">三聯式發票怎麼開?複寫紙用法?手把手開給你看!</a>的圖片來解析,結果如下圖:</p><img src="/2025/07/25/dify-handwritten-invoice/06.png" class="" title="結果"><p>辨識結果準確,處理時間約 6.5 秒,且推論成本低,可滿足企業對效能與成本控管的需求,具備導入價值。</p><h3 id="結論"><a href="#結論" class="headerlink" title="結論"></a>結論</h3><p>如果是非手寫圖片,例如車票、名片等等可以用 gpt-4.1-mini 大部份都可以精準地辨識出來。<br>但遇到手寫圖片時,僅靠 GPT 模型的能力可能會有所限制,此時可透過 <a href="https://learn.microsoft.com/zh-tw/azure/ai-services/content-understanding/">Azure AI Content Understanding</a> 先進行辨識,再交由 LLM 結構化輸出資訊。</p><p>手寫發票的資料輸入是營業稅申報流程中最費時的一環,<br>本文介紹的解法,透過 Dify API 結合 Azure AI,即可將這段流程自動化,大幅減少人工作業時間與錯誤風險。</p><p>綜合來看,只要結合 <strong>Azure AI Content Understanding 插件</strong> 與 <strong>LLM 節點</strong>,就能有效完成手寫發票的文字辨識與結構化輸出,成為自動化營業稅資料建檔的強大工具。<br>而整個設定過程在 Dify 中不到 1 分鐘即可完成。</p><h3 id="加入處理-三聯式發票"><a href="#加入處理-三聯式發票" class="headerlink" title="加入處理 三聯式發票"></a>加入處理 三聯式發票</h3><p>如果要加入處理<strong>三聯式發票</strong>可以在 Prompt 中加入 補充說明 如下,</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">補充說明:</span><br><span class="line">- 三聯式發票中,發票號碼格式相同,常出現在頂部,如「VN 02283153」</span><br><span class="line">- 賣方統編(SELLER_UNIFORM_NO)常見於公司資訊欄位,例如「統編:64145404」</span><br><span class="line">- 買方統編(BUYER_UNIFORM_NO)可能以「實費人統編」或類似詞出現</span><br><span class="line">- 日期格式可從「民國年/月/日」或「100-07-22」推導為「中華民國100年7月22日」</span><br><span class="line">- 小計(SUBTOTAL)與稅額(TAX)通常在「銷售額」、「營業稅」等標示後出現</span><br><span class="line">- 總計金額(TOTAL)可能為「發票稅計」、「合計」、「應收金額」或付款方式金額(如「現金470元」)</span><br></pre></td></tr></table></figure><p>這樣 三聯式發票 也可以很快的辨識出來,使用<a href="https://news.ltn.com.tw/news/focus/paper/527444">打了統編 可報帳抵稅// 捶心肝…發票中千萬 不能領</a>的圖片來測試,如下:</p><img src="/2025/07/25/dify-handwritten-invoice/07.png" class="" title="三聯式發票"><h3 id="參考資源"><a href="#參考資源" class="headerlink" title="參考資源"></a>參考資源</h3><p><a href="https://rainmakerho.github.io/2025/06/23/dify-high-speed-rail-ticket-ocr/">使用 Dify 自動辨識高鐵票</a><br><a href="https://digit.make9.tw/startup/entrepreneur/%E4%B8%89%E8%81%AF%E5%BC%8F%E7%99%BC%E7%A5%A8%E6%80%8E%E9%BA%BC%E9%96%8B%EF%BC%9F%E8%A4%87%E5%AF%AB%E7%B4%99%E7%94%A8%E6%B3%95%EF%BC%9F/">三聯式發票怎麼開?複寫紙用法?手把手開給你看!</a><br><a href="https://learn.microsoft.com/zh-tw/azure/ai-services/content-understanding/">Azure AI Content Understanding</a><br><a href="https://github.com/fujita-h/dify-plugin-azure-ai-document-intelligence">dify-plugin-azure-ai-document-intelligence</a><br><a href="https://news.ltn.com.tw/news/focus/paper/527444">打了統編 可報帳抵稅// 捶心肝…發票中千萬 不能領</a></p>]]></content>
<summary type="html"><h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>手寫發票的資料輸入是營業稅申報流程中最費時的一環,傳統人工輸入容易出錯又耗時。<br>本文將介紹,如何透過 Dify API 結合 Azur</summary>
<category term="發票" scheme="https://rainmakerho.github.io/tags/%E7%99%BC%E7%A5%A8/"/>
<category term="Dify" scheme="https://rainmakerho.github.io/tags/Dify/"/>
<category term="手寫發票" scheme="https://rainmakerho.github.io/tags/%E6%89%8B%E5%AF%AB%E7%99%BC%E7%A5%A8/"/>
<category term="Azure AI" scheme="https://rainmakerho.github.io/tags/Azure-AI/"/>
<category term="文件自動化" scheme="https://rainmakerho.github.io/tags/%E6%96%87%E4%BB%B6%E8%87%AA%E5%8B%95%E5%8C%96/"/>
<category term="GPT-4" scheme="https://rainmakerho.github.io/tags/GPT-4/"/>
<category term="營業稅申報" scheme="https://rainmakerho.github.io/tags/%E7%87%9F%E6%A5%AD%E7%A8%85%E7%94%B3%E5%A0%B1/"/>
</entry>
<entry>
<title>使用 JavaScript 整批匯出 Dify Workflows</title>
<link href="https://rainmakerho.github.io/2025/07/01/dify-batch-export-dsl/"/>
<id>https://rainmakerho.github.io/2025/07/01/dify-batch-export-dsl/</id>
<published>2025-07-01T02:20:40.000Z</published>
<updated>2025-07-01T02:53:14.326Z</updated>
<content type="html"><![CDATA[<h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>Dify 可以在 Browser 上手動將 Workflow 匯出成 yml 檔案。<br>那麼,有辦法整批匯出嗎?</p><h3 id="解法"><a href="#解法" class="headerlink" title="解法"></a>解法</h3><p>上網查了一下,找到了<a href="https://blog.toolman.xyz/article/309/">Batch Import and Export of Dify Workflows Using JavaScript</a>這個方式最容易。<br>直接在 Browser 的 Console 模擬手動的方式來匯出,程式如下:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> token = <span class="variable language_">localStorage</span>.<span class="title function_">getItem</span>(<span class="string">'console_token'</span>); <span class="comment">// 從 Local Storage 取得 Token</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 從目前瀏覽器的網址中動態取得網域和 tagIDs</span></span><br><span class="line"><span class="keyword">const</span> origin = <span class="variable language_">window</span>.<span class="property">location</span>.<span class="property">origin</span>; <span class="comment">// 取得目前頁面的網域 (如 http://localhost)</span></span><br><span class="line"><span class="keyword">const</span> urlParams = <span class="keyword">new</span> <span class="title class_">URLSearchParams</span>(<span class="variable language_">window</span>.<span class="property">location</span>.<span class="property">search</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 發送第一個 API 請求取得 myList</span></span><br><span class="line"><span class="keyword">const</span> apiUrl = <span class="string">`<span class="subst">${origin}</span>/console/api/apps?page=1&limit=100&name=&is_created_by_me=false`</span>;</span><br><span class="line"><span class="title function_">fetch</span>(apiUrl, {</span><br><span class="line"> <span class="attr">method</span>: <span class="string">'GET'</span>,</span><br><span class="line"> <span class="attr">headers</span>: {</span><br><span class="line"> <span class="string">'Authorization'</span>: <span class="string">`Bearer <span class="subst">${token}</span>`</span>, <span class="comment">// 包含 Token</span></span><br><span class="line"> <span class="string">'Content-Type'</span>: <span class="string">'application/json'</span></span><br><span class="line"> }</span><br><span class="line">})</span><br><span class="line"> .<span class="title function_">then</span>(<span class="function"><span class="params">response</span> =></span> {</span><br><span class="line"> <span class="keyword">if</span> (!response.<span class="property">ok</span>) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">'Network response was not ok'</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> response.<span class="title function_">json</span>(); <span class="comment">// 自動解析 JSON 為 JavaScript 物件</span></span><br><span class="line"> })</span><br><span class="line"> .<span class="title function_">then</span>(<span class="function"><span class="params">data</span> =></span> {</span><br><span class="line"> <span class="keyword">const</span> myList = data[<span class="string">"data"</span>]; <span class="comment">// 假設資料結構是 { "data": [...] }</span></span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">'成功取得資料:'</span>, myList);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 遍歷 myList 並發送請求</span></span><br><span class="line"> myList.<span class="title function_">forEach</span>(<span class="function"><span class="params">item</span> =></span> {</span><br><span class="line"> <span class="keyword">const</span> id = item[<span class="string">"id"</span>]; <span class="comment">// 取得每個項目的 ID</span></span><br><span class="line"> <span class="keyword">const</span> name = item[<span class="string">"name"</span>]; <span class="comment">// 檔案名稱改為 item["name"]</span></span><br><span class="line"> <span class="keyword">const</span> exportUrl = <span class="string">`<span class="subst">${origin}</span>/console/api/apps/<span class="subst">${id}</span>/export?include_secret=false`</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 發送請求到 export URL</span></span><br><span class="line"> <span class="title function_">fetch</span>(exportUrl, {</span><br><span class="line"> <span class="attr">method</span>: <span class="string">'GET'</span>,</span><br><span class="line"> <span class="attr">headers</span>: {</span><br><span class="line"> <span class="string">'Authorization'</span>: <span class="string">`Bearer <span class="subst">${token}</span>`</span>,</span><br><span class="line"> <span class="string">'Content-Type'</span>: <span class="string">'application/json'</span></span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line"> .<span class="title function_">then</span>(<span class="function"><span class="params">response</span> =></span> {</span><br><span class="line"> <span class="keyword">if</span> (!response.<span class="property">ok</span>) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">`Export request failed for ID <span class="subst">${id}</span>: <span class="subst">${response.statusText}</span>`</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> response.<span class="title function_">json</span>();</span><br><span class="line"> })</span><br><span class="line"> .<span class="title function_">then</span>(<span class="function"><span class="params">exportData</span> =></span> {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Export 成功 - ID: <span class="subst">${id}</span>`</span>, exportData);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 僅取出 exportData["data"]</span></span><br><span class="line"> <span class="keyword">const</span> dataToSave = exportData[<span class="string">"data"</span>];</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 將 JSON 物件轉成非雙引號格式的文字,去掉最外層的引號</span></span><br><span class="line"> <span class="keyword">const</span> jsonString = <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(dataToSave, <span class="literal">null</span>, <span class="number">2</span>)</span><br><span class="line"> .<span class="title function_">replace</span>(<span class="regexp">/\\n/g</span>, <span class="string">'\n'</span>) <span class="comment">// 替換 \n 為實際換行符</span></span><br><span class="line"> .<span class="title function_">slice</span>(<span class="number">1</span>, -<span class="number">1</span>); <span class="comment">// 去掉最外層的雙引號</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// 將資料儲存到本地檔案</span></span><br><span class="line"> <span class="keyword">const</span> blob = <span class="keyword">new</span> <span class="title class_">Blob</span>([jsonString], { <span class="attr">type</span>: <span class="string">'application/x-yaml'</span> });</span><br><span class="line"> <span class="keyword">const</span> fileName = <span class="string">`<span class="subst">${name}</span>.yml`</span>; <span class="comment">// 使用 .yaml 作為檔案名稱</span></span><br><span class="line"> <span class="keyword">const</span> link = <span class="variable language_">document</span>.<span class="title function_">createElement</span>(<span class="string">'a'</span>);</span><br><span class="line"> link.<span class="property">href</span> = <span class="variable constant_">URL</span>.<span class="title function_">createObjectURL</span>(blob);</span><br><span class="line"> link.<span class="property">download</span> = fileName;</span><br><span class="line"> link.<span class="title function_">click</span>();</span><br><span class="line"> <span class="variable constant_">URL</span>.<span class="title function_">revokeObjectURL</span>(link.<span class="property">href</span>); <span class="comment">// 清理 URL 物件</span></span><br><span class="line"> })</span><br><span class="line"> .<span class="title function_">catch</span>(<span class="function"><span class="params">error</span> =></span> {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">`Export 失敗 - ID: <span class="subst">${id}</span>`</span>, error);</span><br><span class="line"> });</span><br><span class="line"> });</span><br><span class="line"> })</span><br><span class="line"> .<span class="title function_">catch</span>(<span class="function"><span class="params">error</span> =></span> {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">'發送 API 請求時發生錯誤:'</span>, error);</span><br><span class="line"> });</span><br></pre></td></tr></table></figure><p>目前 Dify 1.5 版本中,並沒有 <code>tagIDs</code> 的參數,所以我將它從 url 中移除,並將<strong>limit</strong>設定為<strong>100</strong>,<br>所以目前每頁最多可以匯出 100 個workflow。<br>再加上<code>&is_created_by_me=false</code>參數。</p><h5 id="整批匯出-Workflows"><a href="#整批匯出-Workflows" class="headerlink" title="整批匯出 Workflows"></a>整批匯出 Workflows</h5><p>開啟 Dify 網頁,登入後,按 F12 打開開發者工具,點到 主控台 ,然後將程式碼貼上去就可以了,如下圖所示:</p><img src="/2025/07/01/dify-batch-export-dsl/01.png" class="" title="console"><ul><li>註: 如果發生 401 的錯誤,請重新整理網頁後,再將 Javascript 貼上去執行一次就可以了。</li><li>註: 如果要匯出下一頁的資料,請修改 url 中 <strong>page</strong> 的值,將它改成 <strong>2</strong>。</li></ul><h3 id="參考資源"><a href="#參考資源" class="headerlink" title="參考資源"></a>參考資源</h3><p><a href="https://blog.toolman.xyz/article/309/">Batch Import and Export of Dify Workflows Using JavaScript</a></p>]]></content>
<summary type="html"><h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>Dify 可以在 Browser 上手動將 Workflow 匯出成 yml 檔案。<br>那麼,有辦法整批匯出嗎?</p>
<h3 id=</summary>
<category term="Javascript" scheme="https://rainmakerho.github.io/tags/Javascript/"/>
<category term="Dify" scheme="https://rainmakerho.github.io/tags/Dify/"/>
<category term="Batch Export" scheme="https://rainmakerho.github.io/tags/Batch-Export/"/>
<category term="Workflow" scheme="https://rainmakerho.github.io/tags/Workflow/"/>
<category term="DSL" scheme="https://rainmakerho.github.io/tags/DSL/"/>
<category term="App" scheme="https://rainmakerho.github.io/tags/App/"/>
<category term="yml" scheme="https://rainmakerho.github.io/tags/yml/"/>
</entry>
<entry>
<title>如何在 Roo-Code 和 Kilocode 中設定 Azure OpenAI(AOAI)</title>
<link href="https://rainmakerho.github.io/2025/06/30/setup-azure-openai-roo-code-kilocode/"/>
<id>https://rainmakerho.github.io/2025/06/30/setup-azure-openai-roo-code-kilocode/</id>
<published>2025-06-30T09:50:08.000Z</published>
<updated>2025-07-01T00:44:18.956Z</updated>
<content type="html"><![CDATA[<h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>在 VSCode 中使用 Roo-Code 或 Kilocode 的 AI 助理時,<br>可以讓我們設定各種 LLM API Provider,<br>但裡面却没有提供 Azure OpenAI 的設定方式。<br>那麼,如果使用 Azure OpenAI ,要怎麼設定呢?</p><h3 id="解法"><a href="#解法" class="headerlink" title="解法"></a>解法</h3><p>使用 Azure OpenAI 的 API Provider 設定如下:</p><ol><li>API Provider 請選擇 <strong>OpenAI Compatible</strong></li><li>Base URL 請填写 Azure OpenAI 的完整 URL,例如 <code>https://你的.openai.azure.com/openai/deployments/你的部署名稱/chat/completions?api-version=2025-01-01-preview</code></li><li>API Key 填入 AOAI 的 API Key</li><li>Model 請填入使用的模型名稱,例如 gpt-4.1</li><li>勾選<strong>Use Azure</strong></li></ol><p>如下圖所示:</p><img src="/2025/06/30/setup-azure-openai-roo-code-kilocode/01.png" class="" title="API Provider"><p>之後就可以開始使用 <a href="https://github.com/RooCodeInc/Roo-Code">Roo-Code</a> / <a href="https://github.com/Kilo-Org/kilocode">Kilocode</a> AI 助手了。</p><h3 id="參考資源"><a href="#參考資源" class="headerlink" title="參考資源"></a>參考資源</h3><p><a href="https://github.com/RooCodeInc/Roo-Code">Roo-Code</a><br><a href="https://github.com/Kilo-Org/kilocode">Kilocode</a></p>]]></content>
<summary type="html"><h3 id="問題"><a href="#問題" class="headerlink" title="問題"></a>問題</h3><p>在 VSCode 中使用 Roo-Code 或 Kilocode 的 AI 助理時,<br>可以讓我們設定各種 LLM API Provid</summary>
<category term="Azure" scheme="https://rainmakerho.github.io/tags/Azure/"/>
<category term="VSCode" scheme="https://rainmakerho.github.io/tags/VSCode/"/>
<category term="OpenAI" scheme="https://rainmakerho.github.io/tags/OpenAI/"/>
<category term="AOAI" scheme="https://rainmakerho.github.io/tags/AOAI/"/>
<category term="Roo-Code" scheme="https://rainmakerho.github.io/tags/Roo-Code/"/>
<category term="Kilocode" scheme="https://rainmakerho.github.io/tags/Kilocode/"/>
<category term="API Provider" scheme="https://rainmakerho.github.io/tags/API-Provider/"/>
</entry>
</feed>