Codeer.LowCode.Blazor 向けの拡張フィールドライブラリ。
カレンダー、ガントチャート、カンバンボードなど、ローコードフレームワークに高機能UIコンポーネントを追加する。
NuGetパッケージ Codeer.LowCode.Blazor.Extras (MIT) として配布。
- リポジトリ: https://github.com/Codeer-Software/Codeer.LowCode.Blazor.Extras
- ターゲット: .NET 8.0
- 言語: C# (Nullable有効, ImplicitUsings有効)
- ライセンス: MIT
- 本体リポジトリ:
C:\tfs\codeer\Codeer.LowCode.Blazor(参照用、直接編集しない)
Codeer.LowCode.Blazor.Extras/
├── Source/
│ ├── Codeer.LowCode.Blazor.Extras/ # メインライブラリ (Razor Class Library)
│ │ ├── Components/ # Blazor UIコンポーネント (.razor + .razor.css)
│ │ ├── Designs/ # フィールドデザインクラス (JSON定義)
│ │ └── Fields/ # ランタイムフィールドクラス
│ └── Example/Extras/ # 動作確認用サンプルアプリ
├── Experiments/ # 実験的プロトタイプ (統合前の検証用)
│ ├── BlazorMyCalendarApp/ # カレンダーUI実験
│ └── GanttBlazor/ # ガントチャートUI実験 (SVGベース)
├── bk/ # バックアップ
│ └── GanttBlazor/ # ガントチャート旧版
└── LICENSE, README.md
<PackageReference Include="Codeer.LowCode.Blazor" Version="1.2.45" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.8" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />本体 Codeer.LowCode.Blazor はNuGetパッケージとして参照。
dotnet build Source/Codeer.LowCode.Blazor.Extras/Codeer.LowCode.Blazor.Extras.csprojGeneratePackageOnBuild: True でビルド時にNuGetパッケージを自動生成。
Extras の機能 (メール・ジョブ・承認・AI チャット・意味検索の再索引・自分の認証アプリの解除・Excel → PDF・AI 文書解析) がホストと話す口は 1 本。各機能の節の「結線」はここを指す。
- クライアント (Extras
Api/):ExtrasTransport(static)。EndPoint(既定 "/api/extras") へExtrasRequest {Feature, Action, Payload (操作ごとの要求の JSON)}を POST し、ExtrasResponse {Payload}を受ける。HTTP を使わないホストはHandler(Func<ExtrasRequest, Task<ExtrasResponse?>>) を設定する。機能名・操作名は各機能のXxxApiNames(Mail: Send / SendBulkSearch / Preview / PreviewBulkSearch、Job: Run / Status、Approval: Execute、AIChat: Send / Status / Cancel、SemanticSearch: Reindex / Status / Cancel、Totp: Status / Reset、Excel: ConvertPdf、AITextAnalyze: AnalyzeFile / AnalyzeText)。ファイルの本文は要求のbyte[](JSON では base64) で運ぶ - サーバー (Extras.Server
Api/):ExtrasApiのHandlersにIExtrasApiHandlerを並べ、ExecuteAsync(ExtrasRequest, ExtrasApiContext)が Feature で振り分ける (並べていない機能は null = ホストは 404)。ExtrasApiContext= ModuleDataIO / DesignData / DbAccess / UserId (必須) / Logger / Audit。ハンドラ =MailApi(MailConfig, Func<string, IMailSender?>)/JobApi(IReadOnlyCollection<string> jobNames)/ApprovalApi(MailConfig, Func<string, IMailSender?>)/AIChatApi(AIChatService)/SemanticSearchApi(SemanticSearchService, Func<string userId, SemanticSearchReindexScope>)/TotpApi(TotpLoginSettings)/ExcelApi()(変換は Excel.Report.PDF。フォントのGlobalFontSettings.FontResolverはホストの Program.cs が置く) /AITextAnalyzeApi(AISettings)(中身は AITextAnalyzeService。権限・デザインの拒否はそのまま、モデル側の失敗は "AI analysis failed. Retrying may succeed.")。要求はExtrasApi.ReadPayload<T>で読み、知らない操作はExtrasApi.UnknownActionの例外 - 監査ログ: 分類はハンドラの
GetAuditCategory(action)(旧 Controller の[Audit]属性に当たる)、操作名は "機能.操作" (例 "Mail.Send")。試行の行はExtrasApiが実行前にAuditContext.StartAsyncで書く (前段 = 属性の分類で書いていなければ)。対象・失敗はハンドラがcontext.Auditに入れる。Excel.ConvertPdf は Export、AITextAnalyze は Other - ホスト (Starter Cookie):
Controllers/ExtrasController.cs([Authorize, AutoValidateAntiforgeryToken]・POST api/extras。ExtrasApiTable.Create()の ExtrasApi に DataService から組んだ ExtrasApiContext を渡す) +Extras/ExtrasApiTable.cs(Handlers に 8 行)。クライアントの ServiceInitializer にこれらの機能の EndPoint 代入は無い。デスクトップ (Wpf / WinForms) は従来のフック (MailTransport.Handler/XxxCoreAsync) のままで、ExtrasTransport.EndPoint = string.Emptyにして HTTP に出さない - 互換 (旧結線はそのまま動く): MailTransport は
HandlerかSendMailEndPointが設定されていれば専用ルート。JobTriggerField はRunCoreAsync→EndPoint→ 共通の口。Approval (ApprovalTransport.EndPointBase) / AIChat (AIChatField.EndPoint。デスクトップはSendCoreAsync) / SemanticSearch (SemanticSearchField.EndPoint。デスクトップはReindexCoreAsync) / Totp (TotpResetClient.StatusEndPoint・ResetEndPoint) は専用の EndPoint が設定されていればそちら、空なら共通の口。Excel はConvertPdf(差し替え) →ConvertPdfEndPoint→ 共通の口。AITextAnalyzerField はFileToModuleDataCoreAsync/TextToModuleDataCoreAsync(差し替え) →FileToModuleDataEndPoint/TextToModuleDataEndPoint→ 共通の口 - 共通の口に乗らないもの: 一括ファイル入出力 (
BulkFileReader.ParseFileEndPoint/BulkFileTransferService.ListFileByDataEndPoint/BulkSubmitEndPoint。受け口は本体の ModuleDataController) と本体の ModuleDataController。ServiceInitializer に残る EndPoint 代入はこの 3 行だけ - テスト:
Test/Api/ExtrasApiTest
すべて実装済み。各フィールドのユーザー向け詳細は docs/Xxx.md を参照。
- 状態: 実装済み (docs/CalendarField.md)
- 機能: 月・週・日表示のカレンダー。イベントの表示・追加・編集、モジュールデータとの連携
- インターフェース:
IDisplayName,ISearchResultsViewFieldDesign,IFillHeightFieldDesign - ファイル:
Designs/CalendarFieldDesign.cs,Fields/CalendarField.cs,Components/CalendarFieldComponent.razor
- 状態: 実装済み (docs/GanttField.md)
- 機能: SVGベースのガントチャート。タスクのドラッグ移動・リサイズ、依存関係の管理
- インターフェース:
IDisplayName,ISearchResultsViewFieldDesign,IFillHeightFieldDesign - ファイル:
Designs/GanttFieldDesign.cs,Fields/GanttField.cs,Components/GanttFieldComponent.razor
- 状態: 実装済み (docs/TaskBoardField.md)
- 機能: カンバンボード。ドラッグ&ドロップでステータス変更
- インターフェース:
IDisplayName,ISearchResultsViewFieldDesign,IFillHeightFieldDesign - ファイル:
Designs/TaskBoardFieldDesign.cs,Fields/TaskBoardField.cs,Components/TaskBoardFieldComponent.razor
- 状態: 実装済み (docs/CrossTabField.md)。本体の集計 API (
ModuleDataIO.AggregateAsync/IModuleDataService.AggregateAsync/CrossTabBuilder/ スクリプトのModuleAggregator) の上に乗る「集計定義を受け取って行 × 列の表を描くだけ」の部品。集計・権限・SQL・クロス表の形 (CrossTab) は本体の持ち物 - 機能: 行・列 (複数可・日付は年/四半期/月/週/日)、値 (件数/重複を除いた件数/合計/平均/最小/最大)、行ごと・列ごとの合計と総計、割合表示、値で絞り込み・並べ替え・表示件数の上限。定義は設計 (Setting) か、スクリプトの
Show(aggregator[, rowCount])。CanCustomizeで利用者が自分用に変えられる (表の右上の「集計のカスタマイズ」ボタン (エラー時も出る) /ShowCustomDialog()→ Blazor の CrossTabSettingEditor。保存は localStorage「モジュール名.フィールド名」= ListField のカラムカスタマイズと同じ。読み込み時はReconcileSettingで今の設計に合わせ、OK 時はValidateSettingで検査)。チームで共有する集計は設計に書く - インターフェース:
IDisplayName,ISearchResultsViewFieldDesign,IFillHeightFieldDesign - ファイル:
Designs/CrossTabFieldDesign.cs,Designs/CrossTabSetting.cs,Fields/CrossTabField.cs,Fields/CrossTabCell.cs,Components/CrossTabFieldComponent.razor。デザイナの設定ダイアログは Extras.DesignerControls/CrossTabSettingPropertyControl - リンク越しの項目の解決 (
CrossTabFieldDesign.ResolveField) は LinkField / ModuleField / モジュール参照の SelectField をたどる Extras 側の実装 (LinkTargetModuleName。本体の解決は internal のため使わない)
- 状態: 実装済み (docs/MarkerListField.md)
- 機能: 画像上にマーカー(ピン)を配置・操作。クリック/ダブルクリックイベント
- インターフェース:
ISearchResultsViewFieldDesign,IDataDependentField,IFillHeightFieldDesign - ファイル:
Designs/MarkerListFieldDesign.cs,Fields/MarkerListField.cs,Components/MarkerListFieldComponent.razor
- 状態: 実装済み (docs/QrCodeField.md)
- 機能: 文字列をQRコード画像として表示する表示専用フィールド。参照フィールド(
SourceField)の値、固定文字列(Text)、またはスクリプト(Field.Text)で内容を設定。ECCレベル・前景/背景色を指定可能 - 依存:
QRCoder(MIT, 純C#・System.Drawing非依存)。C#側でPNGを生成し base64 data URI で<img>表示 (JS不要) - インターフェース:
IDataDependentField(SourceField変更に自動追従) - ファイル:
Designs/QrCodeFieldDesign.cs,Fields/QrCodeField.cs,Fields/QrCodeEccLevel.cs,Components/QrCodeFieldComponent.razor
- 状態: 実装済み (docs/ProgressField.md)
- 機能: 進捗率をガント風の角丸バーで表示する表示専用フィールド。値は別フィールド(
ValueField)から、色も別フィールド(ColorField)または固定色(BarColor)から取得。進捗率をバー上に重ねて表示 (ShowValueLabelでON/OFF)、文字色は背景色に対する YIQ コントラスト色を自動選択 (Ganttと同じ)。リストに入れて各行の進捗表示に使える - インターフェース:
IDataDependentField(ValueField/ColorFieldの変更に自動追従) - ファイル:
Designs/ProgressFieldDesign.cs,Fields/ProgressField.cs,Components/ProgressFieldComponent.razor
- 状態: 実装済み (docs/RichTextField.md)
- 機能: 書式付きテキストエディタ。太字・色・リンクなどのHTMLフォーマットに対応
- ファイル:
Designs/RichTextFieldDesign.cs,Fields/RichTextField.cs,Data/RichTextFieldData.cs,Components/RichTextFieldComponent.razor
- 状態: 実装済み (docs/MarkdownField.md)
- 機能: Markdown をプレーンテキストのまま DB 列に保存し、閲覧時 (IsViewOnly) は HTML に描画。編集時はツールバー (見出し/太字/斜体/取り消し線/箇条書き/番号付き/チェックリスト/引用/リンク/コード/コードブロック/表) と
PreviewMode(Tab=タブ切替 / Split=左右並び / None。編集中だけの設定で、読取専用時は常にプレビュー表示。表示名は「プレビュー(読取専用時はプレビューで表示)」= 「なし」なのに閲覧で描画されて見える誤読への対策 2026-09-08)。MaxLength/Placeholder/ShowToolbar。必須と最大文字数の検証。高さの設定は持たない (0.9.1 で Rows 撤去。JSattach/autoSizeが中身に合わせて伸ばす=最小 6 行。FillAvailable の最終行や行 Height で外から高さが決まっているときは「textarea を 0 にしても .markdown-body が縮まない」で判定して伸ばさず stretch+内部スクロール。RichText と同じ振る舞い) - 描画:
Markdown/MarkdownRenderer(Markdig 0.37.0、AdvancedExtensions + 単独改行を<br>+DisableHtmlで生 HTML 無効化 = 信頼できないテキストを描く前提) + リンクに target=_blank。表示 CSS は.markdown-view ::deep。閲覧・プレビューで共通 - 編集の同期: textarea はローカル
_textを oninput で持ち、blur (onchange) / ツールバー操作 / タブ切替でField.SetValueAsync(キー入力ごとに OnDataChanged を起こさない)。外部からの値変更はField.OnDataChangedAsyncで_textへ戻す。ツールバーはwwwroot/markdown-interop.jsのapplyActionが選択範囲を書き換えて全文を返す - RichTextField との住み分け: 書き手が記法を知っている人か AI なら Markdown、記法を知らない利用者なら RichText (HTML 保存)
- 未対応: 検索条件 (SearchLayout)、一覧セル向けの 1 行表示、画像アップロード
- スクリプト:
Value/Html/PlainText/AppendLine(string) - ファイル:
Designs/MarkdownFieldDesign.cs(enumMarkdownPreviewMode同居),Data/MarkdownFieldData.cs,Fields/MarkdownField.cs,Components/MarkdownFieldComponent.razor(.css),Markdown/MarkdownRenderer.cs,wwwroot/markdown-interop.js
- 状態: 実装済み (docs/TagField.md。仕様は docs を先に書いて合意してから実装: 2026-10-07。形は 2026-10-08 に素のフィールドへ)
- 形: タグを付けるモジュールごとにタグ付けモジュール (
TagLinkContractField、役割OwnerId= IdField /TagName= TextField、既定のフィールド名Name)。1 行 = レコードに付いたタグ 1 つ。タグのマスタは無い。TagFieldDesign : FieldDesignBase, IChildRecordsFieldDesign/TagField : FieldBase<TagFieldDesign>, ISearchableField, IOwnedRecordsField(Calendar / Gantt / TaskBoard / MarkerList と同じ形。行はModuleCollection/OwnedRecordModules、行のモジュールはModuleLayoutType.None)。設定はTagModuleName(タグ付けモジュール) と Placeholder / IsSimpleSearchParameter / SearchMatchDefaultValue / OnDataChanged (値が変わるフィールドの標準のイベント。Calendar などと同じ)。必須の設定は無い (タグは付けなくてもよい分類。要る画面は OnValidateInput で Tags.Count を見る)。結び付きはGetChildRecordsCondition(modules)(本体 1.3.44) が契約の役割から組み立てる (OwnerId.Value = Id.Value・並び Id・SelectFields= Id / OwnerId / タグ名)。契約が無ければ null (本体の口の約束: 結び付きが決まらない = 子レコードを持たない。本体は null かどうかで一覧のパスかを判定する)。TagFieldは契約と結び付きをインスタンスで 1 回解決して持つ (デザイナは設計を書き換えながら使うので、デザイン側には持たない)。JSON にもデザイナにも出ない。本体はこれで検索の EXISTS・社員.タグの解決・一覧の行への同梱・条件のデザインチェックを行う。データはListFieldData(タグ付け行)。レコードの削除でタグ付け行を消すのはDeleteTogether(既定 true。従属レコードの宣言のOwnedRecordsDesign.DeleteTogether、本体 1.3.55 = 一覧の DeleteTogether と同じ経路で親の削除の前に 1 行ずつ消す。タグ付けモジュールの CanDelete 必須 = 設計チェックTagFieldDesign:2)。false なら本体は消さず DDL の外部キー (ON DELETE CASCADE) に任せる (論理削除の親のタグ行を残したいときも false)。従属レコードの宣言 (IOwnedRecordsFieldDesign.GetOwnedRecords(modules)、本体 1.3.45) も同じ条件を返す (編集履歴の版に入り、復元で戻る) - セットアップ:
Extras.Designer/Setup/TagSetupService(Tools › タグのセットアップ / CLItag-setup --target <Module>)。名前は対象から決める (フィールドTags、モジュール<Module>Tags、テーブルは snake_case 複数形、列owner_id/name、データソースは対象と同じ)。同名のモジュールに契約が無ければ止める。DDL はTagTableDdlが 3 列の表を DB ごとに組む (本体の DDL の文字列は直さない): name = 長さTagField.MaxTagLength(200) NOT NULL、SQL Server はCOLLATE Latin1_General_CS_AS_KS_WS、MySQL はutf8mb4_binで完全一致 (PostgreSQL / SQLite / Oracle は既定で区別する)。owner_id = 対象の Id 列の型 (DB のRawDbTypeName。文字列の型は長さが取れないので既定の長さ + 結果に案内。DB に無ければ手入力の Id は文字列、それ以外は整数)。一意 (owner_id, name) と name のインデックス、外部キー (SQLite は列に書く) - 動き: チップ入力 (Enter かスペースで確定、打った文字のスペース (半角・全角) で分ける。カンマは区切らずタグの一部。区切りの集合は
TagField.Separatorsの 1 か所で、入力欄も JS のキー判定もそれを使う。外へ移ると打ちかけも確定、IME 中は無視、空欄で Backspace は右端を外す)。足し外しは何も書かない (レコードの保存で一緒に)。同一判定は完全一致 (Ordinal)。タグ名は 200 文字まで (入力欄の maxlength・Nameの MaxLength・DDL が同じ定数。超えたら足さずにエラー表示。エラーは呼び出しの始めに消し、同じ呼び出しでほかのタグが足せても残す)。候補 (TagField.GetCandidatesAsync) = 本体の集計 (AggregateCondition+ValueGroup・Count・件数の降順 → 名前の昇順・上位 10・Like打った文字。何も打っていなければLikeを付けない = よく使われている上位) を 1 回。効くのはタグ付けモジュールの読み取り条件 (親の行の条件は効かない)。TagInputComponentはフォーカスで空欄の候補を出す (1 回だけ問い合わせて_topCandidatesに持つ・待たない)。打った文字は 200 ms 待ってから問い合わせ、後から打った文字の結果だけ使う。サーバーの候補をそのまま出す (付いているタグを除くだけ。手元で絞り直さない。文字が変われば前の候補は捨てる)。問い合わせは文字が変わるごとに 1 回 (区切りで確定したときはAddAsyncが残りの文字で)。%_は本体がエスケープするので、呼ぶ側は何もしない (文字のまま一致) - 読み込み: 詳細はタグ付け行を 1 回読む (未保存のレコードは読まない)。一覧の行・スクリプトで読んだレコードは、一覧の読み込みに同梱された行を使い、無ければ表示のためには読まない (行ごとの問い合わせは出さない)。書く (
AddTag/RemoveTag/SetTags/ 復元のLoadOwnedRecordsAsync) 前は、読んでいない保存済みのレコードだけ 1 回読む (知らずに足すと同じタグの行が二重になり、保存で一意インデックスのエラー)。LoadOwnedRecordsAsyncは読んであれば今の行を返す (全件を持つ =HoldsAllRecords)。スクリプトの ModuleSearcher はSelect(e => e.Tags)で同梱させる。未保存のレコード (コピー) に渡された行はタグ名だけ使って新しい行にする (コピー元の行の Id を持たない)。GetDataはタグ付け行を返す (保存時の意味検索の文章がタグ名を読む) - 検索:
ISearchableFieldを直接実装。子のパスTags.Name.Valueに、いずれか =In、すべて =ContainsAll(本体 1.3.42、SQL で判定)。値はタグ名なので条件の復元・URL 共有で問い合わせない、後から付いたタグも反映される。親の検索にLinkFieldNamesで子のタグを置いたとき (社員.タグ) のContainsAllは、親の子の行のタグを合わせて判定する (テスト: 会社 → 社員 → タグ。デザインはファイルから読んだときに本体が LinkFieldNames のフィールドを作るので、テストはTagTestDesigns.Reloadで読み直す)。検索欄は本体 TextField と同じ input-group + 一致の select (IsSimpleSearchParameterで隠す) - 意味検索: TagFieldDesign が
ISemanticSearchTextSource(Extras の意味検索の口) を実装してタグ名を「, 」で並べる (意味検索の側は型名で分岐しない)。サーバーの索引付け (保存時・再索引とも) はGetWithOwnedRecordsAsyncの内部読みで TagField のタグ付け行を従属レコードとして付ける (本体の一覧読みの同梱は子モジュールの閲覧条件で欠けるので使わない)。タグ付けを直接書くと人の文章が作り直されない → 一括でタグを付ける画面も人のレコードを通して保存する (2026-10-06) - 重なり順: 候補の dropdown は
z-index 1040、検索欄 (input-group) では Bootstrap が.form-controlをposition: relative; z-indexの層にするので:focus-withinで入力欄ごと前に出す。デザイン側のGridColumn.AllowOverflowfalse (overflow: clip) にも切られる → 置く列は はみ出しを許す - IME:
wwwroot/tag-interop.jsがisComposing/ keyCode 229 の間はキーを送らない (変換を確定する Enter でタグを作らない)。確定後のinput/compositionendで区切りがあれば .NET が分ける。箱の中の mousedown は preventDefault でフォーカスを入力欄に留める (候補のクリックが blur で消えない) - スクリプト:
Tags/AddTag/RemoveTag/SetTags/HasTag、検索欄SearchTags/SearchMatch(TagSearchMatchをExtrasClientInitializerで登録)。内部用は[ScriptHide] - 一覧のセル: 編集できるセルではチップ + 入力欄、閲覧ならチップだけ。本体の
td.form-control-style-inline input:focusの内側リングは、.razor.cssで同じ:not列を付けた高い詳細度のセレクタで消す (!important は使わない) - ファイル:
Designs/TagFieldDesign.cs(enumTagSearchMatch同居),Designs/TagLinkContractFieldDesign.cs,Fields/TagField.cs,Tag/TagContracts.cs(契約の取得・タグ名の読み方),Components/TagFieldComponent.razor/TagFieldSearchComponent.razor/TagInputComponent.razor(.css),wwwroot/tag-interop.js,Extras.Designer/Setup/TagSetupService.cs(+TagSetupWindow,SetupCliのtag-setup),Extras.SeleniumDrivers/TagFieldDriver.cs(+TagFieldSearchDriver), テストExtras.Test/Tag/(ハーネスHarness/DbClientServices= クライアント Module を SQLite の ModuleDataIO に直結・集計も、TagTestDesigns) とExtras.Test/Setup/TagSetupServiceTest。Example:TagTest+TagTestTags(tag-setup 生成)、SampleData/tag_sample.sql
- 状態: 実装済み (docs/ColorPickerField.md)
- 機能: HTML5ネイティブカラーピッカー。色をHEX文字列として保存
- ファイル:
Designs/ColorPickerFieldDesign.cs,Fields/ColorPickerField.cs,Data/ColorPickerFieldData.cs,Components/ColorPickerFieldComponent.razor
- 状態: 実装済み (docs/AIChatField.md)。サーバー側に標準 Agent (
RawDataAccessAgent。会話エンジンChatClientAgentは internal) あり。Example の既定はダミー (Example/.../Extras.Server/AI/DummyAIChatAgent.cs)、RawDataAccessは AISettings (Azure OpenAI) があるときだけ登録 - 機能: チャット UI だけを担う。入力 (Ctrl+Enter 常に送信 / Shift+Enter 常に改行 / Enter は
SendOnEnter既定 true / IME 対応 / 自動伸長。Placeholder / WelcomeMessage は撤去 = 必要なら画面にラベルを置く。.aichat-messagesは min-height 16rem で初期表示に高さ)、考え中ドット + 途中経過 + 経過秒、逐次表示、エラー + 再送、停止、新しい会話、返事のコピー。返事は HTML をそのまま描画 (SVG グラフ含む) - Agent の選択: デザインの
Agent(文字列、空 = 既定) をAIChatSendRequest.Agentで送り、AIChatServiceがコンストラクタで受けた対応表Func<string, IAIChatAgent?>で解決 (アプリ側の静的クラス。Example はAI/AIChatAgentTable.cs= MailSenderTable と同型、名前ごとに Lazy で 1 つ、Service静的プロパティに AIChatService)。無い名前は error。DI 登録なし。AIChatService.Start(owner, conv, message, agentName)(internal) - 通信: 既定は共通の API (
AIChat.Send/Status/Cancel)。AIChatField.EndPointを設定した専用ルートはPOST {EndPoint}→ 202{requestId}→GET {EndPoint}/{requestId}をポーリング (最初の 10 秒は 1 秒、以後 2.5 秒) →{status, reply, progress, error}。DELETEで中断。やり取りする型はAIChat/(AIChatSendRequest / AIChatSendResponse / AIChatStatusResponse / AIChatJobStatus) - サーバー (Extras.Server
AI/Chat/。フォルダ = 役割: 直下 = 共有の契約と窓口 (IAIChatAgent/AIChatService/ChatReplyHtml)、ChatClient/= 内部の会話エンジン (ChatClientAgent とツールセット機構、Chart ツール。すべて internal)、RawDataAccess/= 専用 (Agent / Options / ToolSet / DbSchemaReader)、DesignKnowledge/= 設計参照ツール (DesignDescriber = DesignData の要約、DesignKnowledgeToolSet = list_modules / describe_module / read_document)。名前空間もフォルダどおり):IAIChatAgent/AIChatService(旧 AIChatJobStore。プロセス内ジョブ。プロセスに 1 つ。対応表 Func か単一 Agent を受ける。FinishedRetention / MaxRunning は init プロパティ = Options 型は無い) /ChatReplyHtml(Markdig で Markdown→HTML、テキストはエスケープ、HTML 素通し + target=_blank) /ChatClientAgent(Microsoft.Extensions.AIIChatClient。システムプロンプト + internal なIAIChatToolSet群、会話履歴ConversationHistory(鍵 = 所有者+会話ID (所有者空なら会話IDのみ)。切り詰め 3 段: 古いターンのツール呼出/結果を落とす KeepToolResultsForTurns=2 → ターン数 MaxHistoryTurns=20 → 文字数 MaxHistoryCharacters=40000。いずれもターン境界で)、FunctionInvokingChatClient で往復上限、ストリーミング → ReportPartial、Markdown→HTML→ToolSet.PostProcessHtml) /RawDataAccess/RawDataAccessToolSet(get_schema =DbSchemaReaderで DB 種別ごとのカタログ問い合わせ + モジュール定義の業務名、execute_sql = 1 文 SELECT 判定Validate+ DbCommand 直叩き (タイムアウト・MaxRows・MaxResultChars) + ログ) /ChatClient/ChartToolSet+SvgChart(render_chart →[[chart:N]]プレースホルダ → 後処理で<div class="aichat-chart"><svg>) /RawDataAccessAgent(ChatClientAgent を包含して委譲 + 上 2 ツールセット。継承ではない。ChatClientAgent は sealed) - 公開面: public は契約 (
IAIChatAgent/AIChatAgentRequest/AIChatReply/AIChatReplyFormat/IAIChatProgress)、窓口 (AIChatService。★2026-09-24 AIChatJobStore + Options を 1 型に畳んだ破壊的変更)、標準 Agent (RawDataAccessAgent+RawDataAccessOptions。意味検索はsemanticSearch: SemanticSearchService?で受ける。会話の設定 (プロンプト・履歴上限・逐次表示) も RawDataAccessOptions に、ILoggerFactory はコンストラクタ) だけ。ChatClientAgent/ChatClientAgentOptions/IAIChatToolSet/AIChatToolContext/RawDataAccessToolSet/ChartToolSet/SvgChart/ChatReplyHtml/ConversationHistoryは internal (テストは IVT)。独自ツールの Agent はアプリがIAIChatAgentを直接実装する (ToolSet を公開しない = 実装の詳細) - 設計と文書: 業務の意味はデザインにある。
RawDataAccessAgent(clientFactory, dbAccessorFactory, Func<DesignData?>? design, Func<IReadOnlyList<AIChatDocument>>? documents, options)。design があれば DesignKnowledgeToolSet が付き、モデルは list_modules/describe_module で業務語→表・列・候補値・リンク・スクリプトを確かめる (QueryField の SQL は .sql ファイル=実行エンジン専用のバッファなので出さない)。文書 (AIChatDocument) の出所はホスト = デザインのResources/{DocumentFolder}/*.md(フォルダはAIChatFieldDesign.DocumentFolder→AIChatSendRequest.DocumentFolder→AIChatAgentRequest.DocumentFolder。空 = 文書なし。読み出しは本体 1.3.32 のDesignDataFileManager.GetResourceTexts(dir, folder, ".md", ".txt")(App.zip の Resources/{folder} 直下) を Example / Starter の対応表が呼ぶ)。IAIChatToolSet.GetInstructions(context)(依頼ごとに文書が変わるためプロパティから引数付きに)。Example デザインにResources/AIChat/データの見方.md。文書は合計 8000 字以下なら全文をプロンプトに入れる (実 AI 検証: 一覧だけだと read_document を呼ばず「売上」を全件合計した→全文注入で定義どおり完了分のみになった)、超えたら抜粋 200 字 + read_document - 画面 URL:
DesignDescriber.PageUrlsがページフレームのサイドバーリンク (Module 一致 → PageFrame/ModuleUrlSegment) から/{frame}/{segment}と/{frame}/{segment}/{Id}を組む (リンク無しはルートのフレーム、フレーム無しは省略)。list_modules と describe_module に出し、プロンプトで「行を挙げるときは詳細リンクを付ける」。ChatReplyHtml.AddLinkTargetsは外部 (http(s)://、//) だけ target=_blank、アプリ内相対リンクは同じタブ - トークン節約 (2026-09-09): get_schema は 2 段 (引数なし = 表名+列数+モジュールの目次 /
tables指定 = その表だけtable(col TYPE [説明], …)の 1 行)。方言はRawDataAccessToolSet.GetInstructionsに常時 (Dialects()= DataSource 種別のみ・接続なし)。プロンプトで順序を明示: 文書 → describe_module (表と列が出る) → 足りないときだけ get_schema。列情報は全データソース分を 10 分キャッシュ (LoadSchemaAsync)。ツール結果は履歴で直近 2 ターン後に落ちる - 複数データソース:
RawDataAccessOptions.DataSourceNames(IList)。get_schema はデータソースごとに## データソース X+ 方言、execute_sql はdataSource引数 (1 つなら省略可)。設計の業務名は「モジュールの DataSourceName が一致するもの優先、無ければ表名で全モジュール」。意味検索 (search_records) の接続も同じ見方 (SemanticSearchToolSet.ResolveDataSource: 一覧にモジュールの DataSourceName があればそれ、無ければ別名 (どのモジュールも名指ししていない名前) のうち同じ DB 種別のもの。2026-09-30 別名構成で対象モジュール無しになる不具合の修正) - 方針 (ユーザー決定 2026-09-09): 権限管理はライブラリで持たず DB 側の設定 / 接続文字列 (AI 用読み取り専用ユーザー・列 GRANT・ビュー) で。基本的にデータは AI に渡してよい (Azure OpenAI は学習不使用・リージョン内)。気にする顧客 (個人情報・契約・業法) には列 GRANT / ビュー / 閉域・ローカルモデルの 3 手段。docs/AIChatField.md「AI に送られるデータ」に記載。除外列オプション等は要望が出てから ★2026-10-02 監査方針確定 (ユーザー): RawDataAccess は「AI 用 DB ユーザー (個人情報を除いた / マスクしたビュー) で範囲を固定 + 監査ログの送信の行 (AIChat.Send) で利用の事実」で監査基準に対応。行単位の閲覧記録が問われるのは個人情報系だけなので範囲から外せば対象が無くなる。SQL 文の監査ログ化・読んだ行の Id は作らない (値が証跡に残る・集計 SQL では行を特定できない)。ILoggerFactory の実行ログは事故調査用。docs/AIChatField.md「監査基準への対応」+ Extras.Designer SpecDocs/AuditLog.md「聞かれたときの答え方」(CCFD 向け)
- DB の負荷の上限 (2026-10-06):
RawDataAccessOptionsのCommandTimeoutSeconds(既定 30) /MaxQuerySecondsPerReply(既定 0 = 無効。予算はQueryTimeBudgetをAIChatToolContext.Itemsに) /MaxConcurrentQueries(既定 0 = 無効。QueryGate= データソース名+上限ごとの static SemaphoreSlim) /CancelQueryAtRowLimit(既定 true。MaxRows 超でcommand.Cancel()、後始末の例外は打ち切り時だけ握る) /LargeTableRows(既定 100000。DbSchemaReader.ReadTableStatsAsync= カタログの行数と索引の先頭列。読めなければ空。プロンプトに大きい表を最大 30 件・目次と表指定に行数)。0 / false で無効。待たせる・断る種類 (予算・同時実行) は既定で無効 (バージョンを上げただけのアプリの動きを変えない)。SQL の書き換え (LIMIT 被せ) と EXPLAIN はしない。ホストは appsettingsAIChat:RawDataAccessを束縛して渡す。詳細は docs/AIChatField.md「DB の負荷を抑える」 - ModuleDataAccessAgent (0.18.0・2026-10-02): 権限の効く Agent。
AI/Chat/ModuleDataAccess/=ModuleDataAccessAgent(clientFactory, Func<string userId, Task<ModuleDataAccessScope>> openScope, Func<DesignData?> design, documents, ModuleDataAccessOptions, ILoggerFactory?)+ internalModuleDataAccessToolSet(find_records / aggregate_records / cross_tab。filters は項目の比較か all / any のグループ (入れ子)・not = MultiMatchCondition の木) +ModuleDataConverter(AI の条件 → SearchCondition / AggregateCondition、ModuleData → 行 JSON、集計の鍵 →{ value, text })。行は実行ユーザーのModuleDataIO.GetListAsyncで読む (UserRead / DataRead / PermissionField / 論理削除が画面と同じに効く)。ユーザー Id =AIChatAgentRequest.UserName(ownerKey)。スコープはツール呼び出しごとに開閉 (ホストはnew DataService(userId)を包む)。集計は本体の集計 API (2026-10-06 差し替え): aggregate_records =AggregateCondition(ValueGroup / DateGroup (year〜hour・FiscalYearStartMonth)・AggregateMeasure (count / countDistinct / sum / avg / min / max)・Having・Sort・LimitCount = MaxGroups) →ModuleDataIO.AggregateAsync、cross_tab =CrossTabBuilder.BuildAsync(condition, rows.Length, ModuleDataIO.AggregateAsync, withTotals, MaxCrossTabCells)。読めない項目の集計は本体が拒否 (accessDenied)。並び未指定はグループの昇順 (cross_tab は行の項目だけ)。メモリ集計 (MaxAggregateRows) は廃止。負荷の上限 (同日): 読み取り 4 ツール (find_records / get_record / aggregate_records / cross_tab) はReadAsyncの外枠を通る =QueryTimeBudget(MaxQuerySecondsPerReply) →QueryGate(MaxConcurrentQueries・鍵はモジュールの DataSourceName・RawDataAccess と共有) →ModuleDataAccessScope.DbAccessor.CommandTimeoutSeconds(CommandTimeoutSeconds。core 1.3.41 のIDbAccessor.CommandTimeoutSeconds(既定実装 0/無視) + DbAccess 0.2.0 の DbAccessor 実装。ホストは Scope にds.DbAccessを渡す) → 時間切れ/拒否を AI 向けエラーに。既定は全部 0 (無効)。get_record= Id 1 件 + 子一覧 (ListField / ModuleField。子の項目はその一覧レイアウトの項目 = 本体のSetLayoutFilter。孫は読まない)。★罠: リンクの表示名は本体がLinkFieldData.DisplayTextを埋めない = リンク越しフィールド ("Customer.Name" を名前に持つ列) を一緒に読んで表示名にする (DisplayTextPath)。無い設計では値だけ。SQLite の TEXT 日付列は DateOnly→ToString() (カルチャ依存) で文字列比較になる (core の仕様。テストは DATE 型)。読んだ行の監査ログは未対応 (SuppressFlow のためジョブに AuditContext は無い)。テストTest/AI/ModuleDataAccessToolSetDbTest(SQLite 18 本 + 実 AI 1 本 Explicit) - 意味検索を権限つきで (2026-10-06): core 1.3.41 の
FieldSqlMatchCondition(抽象・FieldName) +ISqlMatchConditionFieldDesign(CreateWhere / CreateOrderBy) +SqlConditionContext(DataSourceType / Column / AddParameter) = フィールド型が自分への条件の SQL を作る口。本体は一覧検索の SQL に断片を入れ、モジュールの閲覧権限・DataRead・論理削除を外側で掛け、対象フィールドの読み取り権限 (PermissionField) を見る。メモリ評価 (IsMatch) は例外・設計の条件に置くと設計チェックMatchConditionsExtensions:2。Extras 0.18.0 のSemanticMatchCondition(Vector / MaxDistance) をSemanticSearchFieldDesignが実装 (pgvector<=>/ SQL Server 2025VECTOR_DISTANCE。並びは SortConditions に "Search.Value")。ベクトルは呼び出し側が作るか、Text だけ入れてサーバーの前処理に作らせる (下記)。ベクトルは SQL にパラメータで渡す (CAST(@p AS VECTOR(n))/@p::vector。リテラルだと検索ごとに SQL 文が変わりプランを毎回作り直す)。ModuleDataAccessAgent(..., semanticSearch:)でsearch_records(find_records と同じ行の形・近い順・score なし)。画面の検索欄 (同日): 検索レイアウトに置くとSemanticSearchSearchComponent(文章の入力欄)。SemanticSearchField : ISearchableFieldがFieldMatchCondition{ SemanticMatchCondition{ Text, MaxDistance = SearchMaxDistance } }を作り、サーバーのSemanticSearchService.Interceptor(0.22 で ConditionInterceptor から改名・旧名は Obsolete の別名。core 1.3.41 のIModuleDataIOInterceptor.PrepareConditionAsync。ホストの CustomizedModuleDataIO で AddInterceptor) が Text → Vector (権限の判定の後に呼ばれる = 拒否される要求では埋め込み API を呼ばない)。並びは一覧の SortConditions に "Search.Value" を入れる (条件が無いと本体が飛ばす)。サーバーで並びを差し込むことはしない。SearchMaxDistanceが空で、その検索レイアウトを使う一覧 (ページの一覧・SearchField の結果・LinkField の検索ダイアログ) の並びに無いと文章が効かないので設計チェックSemanticSearchFieldDesign:3。実 pgvector (Azure PG の Selenium 用 DB) と Azure SQL (VECTOR 型) で生 SQL 版と同じ行・同じ順、距離の上限つきの一覧と集計が同じ行を数えることを確認 (SemanticSearchVectorDbTest) - 権限の考え方: RawDataAccess は DB を生で読む。見える範囲 =
RawDataAccessOptions.DataSourceNamesの接続 (AI 用の読み取り専用 DB ユーザーを GRANT で絞る) で決める。ログインユーザーごとの行制限は効かないので、置くページの UserReadCondition で使える人を絞る。SELECT 判定は補助 - IChatClient の作り方: Azure OpenAI は Extras.Server
AI/AzureOpenAIClients.cs(ChatClientFactory(AISettings)。埋め込みはここではなく IEmbeddingProvider へ (2026-09-24)。設定が欠けていれば null。AzureOpenAIClient は 1 つを使い回し・ApiKeyCredential) が提供 (2026-09-23。それまでは Example/Starter の AIChatAgentTable.CreateAzureOpenAI がホスト側で作っていた)。Agent / AITextAnalyzeService / SemanticSearchService 自体は Microsoft.Extensions.AI の抽象しか受け取らないので、別プロバイダはホストが同じ型を作って渡す - 結線: 共通の API の
AIChatApi(AIChatService)(テンプレは ExtrasApiTable にAIChatAgentTable.Service)。デスクトップはAIChatField.SendCoreAsyncフック。専用ルートはAIChatField.EndPoint(互換)。Example の appsettingsAIChat:RawDataAccessDataSource(既定 SampleSQLite、SystemConfig.AIChat)。テスト環境: gitignore 済appsettings.AIChatTest.json(キー入り) + プロファイル「Extras.Server (AIChatTest)」https://localhost:7469 - 会話の保持 (0.11.0): 会話は
AIChatConversation(発言・会話 ID・ポーリング) としてAIChatConversationStore(DI スコープのServicesに ConditionalWeakTable で紐づく = ホスト登録なしでスコープと同じ寿命。鍵{Module}:{Field}。静的なのは弱参照の索引だけ。★これが正式な形。Extras クライアントはホストに DI 登録を求めない = 登録必須化は既存ホストを壊す破壊的変更なのでやらない) に置き、AIChatFieldは表示だけの薄い層 (Changed/ReplyReceived の委譲、コンポーネント Dispose でDetachAsync)。ポーリングは会話側で続くので離脱中に返事が届く。sessionStorage は使わない (リロードで消えるのは意図)。KeepConversation=falseなら保管庫に置かず離脱で中断。送信時の写し (AIChatSendRequest.Transcript: テキストのみ・直近 6 往復・4000 字) はサーバー履歴が消えたときの文脈復元用で、ChatClientAgentは履歴が無いときだけConversationHistory.Seed - 複数インスタンス: ジョブと会話履歴はプロセス内 → セッション固定 (ARR アフィニティ) 前提。必要になれば DB テーブルの共有ストアに差し替える (本体の一時ファイル表方式)
- ローカライズ: 画面に出る文言 (進捗・未登録エラー・グラフの「データなし」) は Extras.Server の
Properties/Resources(.ja-JP).resx。AIChatService / SemanticSearchService が Start 時のカルチャをバックグラウンドへ引き継ぐ。モデル向けのプロンプトとツール説明は日本語の定数 (ローカライズ対象外)。リクエストのカルチャはホストのAddLowCodeLocalization()(Extras.ServerWeb/LocalizationExtensions。Accept-Language で ja-JP / 中立) +UseRequestLocalization()で決まる - テスト:
Test/AI/(AIChatServiceTest = ジョブ + 対応表の振り分け / ChatClientAgent は台本つきFakeChatClient/ RawDataAccessToolSet は SQLite 実 DB / SvgChart) - インターフェース:
IFillHeightFieldDesign(Height=0 で親の高さに合わせる) - ファイル:
Designs/AIChatFieldDesign.cs,Fields/AIChatField.cs,Components/AIChatFieldComponent.razor(.css),wwwroot/aichat-interop.js,AIChat/*.cs - 意味検索 (0.14.0): コンストラクタ末尾の
Func<IEmbeddingProvider>? embeddingProviderを渡し、design に SemanticSearchField (3 列あり・PostgreSQL / SQL Server) のモジュールがあればSemanticSearch/SemanticSearchToolSet(internal) が付きsearch_records(moduleName, query, top)が使える (下の SemanticSearchField 参照)
- 状態: 実装済み (docs/SemanticSearchField.md, Extras.Designer FieldDocs/SemanticSearchFieldDesign.md)。Example にはホスト側の結線だけ (
AI/SemanticSearchIndex.cs= SemanticSearchService の静的な持ち物 (Service。プロバイダは Lazy で EmbeddingProviderTable から) +AI/EmbeddingProviderTable.cs/ CustomizedModuleDataIO / AIChatAgentTable)。再索引 API は 共通の API のSemanticSearchApi。Example のデータは SQLite なので意味検索できるモジュールは置いていない (Inquiryは通常モジュール) - 形: 書き込み専用列 2 本 (
DbColumnText文章 /DbColumnVector=[0.1,-0.2,…]JSON 配列テキスト) + DB のベクトル型列DbColumnVectorSearch(PG はsearch_vector::vectorの生成列、SQL Server 2025 は VECTOR 型列にテキストから暗黙変換で書けるのでDbColumnVectorと同名)。3 列すべて必須 (HasColumns)。UI なし (PasswordHashField と同型。FieldDesignBase直下)。本体 (core) は触らない - ★検索は DB 側だけ (2026-09-23 決定): サーバーでの全行メモリ比較 (コサイン類似度) は廃止。
SemanticSearchIndexReader.SearchAsync(BuildSearchSql: PG<=>+limit / SQL ServerVECTOR_DISTANCE('cosine')+top、semantic_score= 1-距離、論理削除はcast(col as integer)=0) のみ。SemanticSearchVector.SupportsDbSearch(type)= PostgreSQL / SQLServer。それ以外の DB のモジュールはSemanticSearchToolSet.SearchableModules(データソース定義の種別で判定・接続しない) から外れ、AI への説明にも出ない。DB 検索の失敗は AI にエラーとして返す (フォールバック無し)。SemanticSearchVector(Extras 本体・internal) が保存形式 (Encode) と DB の書き方 (SupportsDbSearch / FromText / Literal / CosineDistance) を 1 か所で持つ (生 SQL の search_records・{embed:} と、SemanticSearchFieldDesign の権限つき検索の両方が使う。2026-10-06 に方言の重複を解消) - execute_sql の
{embed:…}:RawDataAccessToolSet.SqlPreprocessor(Func<ds, sql, ct, Task>) を RawDataAccessAgent がSemanticSearchToolSet.ExpandEmbeddingsAsyncに結線。SELECT 判定より前に置換 (数値リテラルだけになる)。非対応 DS は例外→AI にエラー。ログは展開前の SQL。AI への説明は GetInstructions が意味検索できるモジュールごとに表・ベクトル列・方言ヒント (DialectHint) を列挙 - 文章:
SemanticSearch/SemanticSearchText.Build(Extras。作るのはサーバーだけ) が「表示名: 値」を LF 区切りで 1 行ずつ。SourceFields空 =DefaultSourceFields(DB 列ある入力フィールド全部。Id / 論理削除 / 楽観ロック / Password / 作成更新記録 / 自身は除く)。候補値は表示名、リンクは DisplayText、日付 ISO、RichText はタグ除去。MaxTextLength既定 8000 - 添付の中身の索引 (0.22.0):
SourceFieldsに名前を書いた FileField だけ (既定の対象には入らない)。文章を作るのは Server のSemanticSearchService.BuildTextAsync1 か所 (Interceptor と Reindex が呼ぶ。添付を読んでSemanticSearchText.Build(..., attachmentTexts)に渡す)。ストレージの添付はFileStorages(Func・既定 null = ファイル名だけ) があるときだけ、DB 列格納モードは行の Content。取り出しはFileManagement/FileTextExtractors(拡張子 →IFileTextExtractor。IEmbeddingProvider / IMailSender / IFileStorage と同じインターフェイス + 対応表の形。Addは実装の拡張子を全部登録し後勝ち・Removeは拡張子 1 つ・Findで引く。既定の実装はFileManagement/TextExtractors/に 1 クラスずつ: Pdf=PdfPig / Word・PowerPoint=Open XML SDK / Excel=ClosedXML キャッシュ値 / PlainText (BOM → UTF-8 厳密 → Shift_JIS) / Html タグ除去)。拡張子で先に判定して対応表に無ければ開かない。例外は警告ログで保存は止めない。1 件MaxFileTextLength(既定 100000・0 = 無制限) で切る。テスト:AI/FileTextExtractorsTest/SemanticSearchAttachmentDbTest - ★索引はサーバーが保存した行から作る (2026-10-12 ユーザー決定「仕様をシンプルに」・2026-10-13 13 回目で「変更したレコード」規則に置換):
SemanticSearchService.Interceptor(SemanticSearchInterceptor。旧 ConditionInterceptor は Obsolete の別名) のSubmitAsync。★作り直す行の決め方は送信の形を読まず、本体 core 1.3.50 のSubmitContext.ChangedRecords(インターセプタの SubmitAsync の引数 context。この保存で変更したレコード = 本体の書き込み合流点で記録。追加は採番後の Id・内部書き込みも含む・DB 採番の一括 INSERT は含まない) で決める。規則 2 つ: (1) Add / Undelete の行は全部、Update の行は SourceFields の項目を書いたフィールドだけ。IsBulkInsertの行 (一括 INSERT の手入力 Id) は除く = 取込は Id の付け方によらず索引を付けない (14 回目。2026-10-15 に文章も付けないに変更) (2) SourceFields の従属宣言 (IOwnedRecordsFieldDesign.GetOwnedRecordsの子モジュール = タグ付け行) のモジュールの行を Add/Update/Delete/Undelete したら、その親の行 (17 回目:IsBulkInsertの行は (1) と同じく除く = タグ付けモジュールの一括取込の親は Reindex) (★14 回目: 親は宣言の束縛 (条件の FieldVariableMatchCondition: 子の項目 = 親の項目) から子の行の値で引く。親の項目が Id ならそのまま・違えば親を検索。消す子 (送信の Delete / SearchDelete) と更新する子 (送信の Update。16 回目: 付け替え前の親も作り直す) は next() の前に読んで親を控える。送信の主レコードで決めていた 13 回目の形は一覧の中の行 (入れ子) のタグだけ変えた保存を取りこぼしていた)。経緯: 6〜12 回目は送信の形 (Add/Update に索引データが無い行・対象フィールドを送った更新・仮 Id の付与と TemporaryIdMap・@replace:の解析・ExtendedData…) を読み解いていて、形が増えるたびに分岐が増え収束しなかった (13 回目で編集履歴の復活 = Add/Update 空の送信、索引データだけの更新 を取りこぼし) → 事実 (何が書かれたか) で決める形に置換。保存の前に送信の全行からこのフィールドのデータを捨てる (索引の列はサーバーだけが書く)。next()の後にio.CreateSystemAccess()のGetWithOwnedRecordsAsyncで読み直し (従属レコードは SourceFields にある宣言だけ・論理削除した行は読めない = 索引しない)、フィールドごとにまとめて埋め込み (EmbedForSaveAsync。失敗は警告ログ + 文章だけ)、同じ口のWriteWithoutSideEffectsAsync(書き込み専用列だけの UPDATE = 更新日時・版・ExecuteSql・ホストの override を通らない) で書く。索引列の書き込み失敗は保存の失敗。★一括 INSERT の行には索引を付けない (2026-10-15 ユーザー決定。文章も付けない = ホストのBulkAddAsyncの結線は不要。取込の後はReindexMissing()= 行から文章を作り (リンク先の表示名も入る) 埋め込む。説明は「保存すればその行の索引ができる。一括取込と穴埋めは ReindexMissing()、全部作り直すのは Reindex()」の 2 文)。★インターセプタで next() の前に文章を入れる案は却下 = フィールドの書き込み権限チェックの前に索引データが入り、このフィールドに PermissionField の書き込み条件 (再索引を絞る推奨構成) を付けた人の追加が拒否される。クライアント (Fields/SemanticSearchField) はIsModified = false・GetSubmitData空 = 何も送らない (Textプロパティ・IDataDependentField・設計チェック:2は削除)。ホスト結線はAddInterceptor(Service.Interceptor)(★他のインターセプタより後 = 最後に登録 = 一番内側。理由は「索引列の書き込み失敗 (= 保存の失敗) が外側の監査ログ・編集履歴の next() を通って上がる」こと。17 回目で訂正: ChangedRecords は SubmitAsync の入口で消して保存中ずっと積むので、順序を入れ替えても復活の行は拾える (外側に置けば拾える範囲はむしろ広い)。内側なので外側が next() の後に書いた行には付かない。★18 回目: 索引付けの失敗は例外で投げず結果の ExceptionMessage で返す (SubmitAsyncの自分の処理 (保存前の親の控え・保存後の索引付け) を try で包みFailへ。next() の中は包まない。権限拒否だけ型のまま上げる)。例外のままだと本体が最外で ExceptionMessage にして HTTP 200 で返すので、外側の監査ログ (AuditContext.RecordSubmitAsync。権限拒否しか catch していなかった) には保存が巻き戻ったのに Success で残っていた。併せてRecordSubmitAsyncも権限拒否以外の例外を Fail にして投げ直す (今後の内側インターセプタにも効く)。保存の前の読みは送信の Id のまま (Id 手打ち変更の行は読めずに飛ぶ = 子の Id 手打ち変更と親の付け替えを同じ保存でやると旧親は作り直さない割り切り。Extras で@replace:を解析しない)。親の束縛は宣言の FieldVariable 条件すべて (AND)) だけ (Obsolete のApplyAsync(data, bool)は何もしない。Interceptor 未登録なら警告ログを 1 回出すだけ。docs に「0.21 以前からの移行」節。14 回目)。再索引はModuleDataIO.CreateSystemAccess()の GetListAsync (Id だけ) + 1 行ずつReadForIndexAsync+ WriteWithoutSideEffectsAsync。文章の列は全文・MaxTextLengthは埋め込みに渡す長さだけ (13 回目。キーワード検索は全文に当たる・添付の中身の構想と同じ線) - サーバー (Extras.Server
AI/SemanticSearch/): ★2026-09-24 ホスト向け公開型はSemanticSearchService1 つ (旧 SemanticSearchIndexer + SemanticSearchReindexJobStore + Options を畳んだ。ctor(Func<IEmbeddingProvider?> embeddingProvider, Func<DesignData> design, ILoggerFactory?)、init プロパティ FinishedRetention / MaxRunning / ReindexPageSize / SaveEmbeddingTimeout。SemanticSearchReindexScopeだけ別型で残る。ユーザー指示「AIChat も破壊的変更でいい・両方やって」)。一括 INSERT 用のApplyAsync(datas)は 2026-10-15 に削除 (未公開。一括の行は索引の列が空のまま・取込後に ReindexMissing。bool 付き旧形式は Obsolete で何もしない)。再索引の入口はFieldApiAuthorization.CheckWriteAsync(core 1.3.50 のModuleDataIO.AuthorizationChecker.CheckFieldUserWriteAsync= UserWriteCondition + フィールドの書き込み権限。10 回目: 読める人全員が全行の埋め込みを起こせていた。12 回目: UserWriteCondition も PermissionField も無い既定ではアプリに入れる全員が起こせるので docs は「絞るならこのフィールドに PermissionField の書き込み条件」と案内)。索引列の書き込み失敗は保存の失敗 (11 回目: best-effort にしたら実 PG で保存が黙って消えた = PG はトランザクション内の 1 文の失敗で以後が全部失敗し COMMIT が ROLLBACK になる。握れるのは埋め込みの失敗だけ)。送信に載ったこのフィールドのデータは保存の前に捨てる (索引の列はサーバーだけが書く)。再索引の合流は同じ種類のときだけ (欠損だけが走る間の全行はSemanticSearch_ReindexMissingRunning)。LoggerFactoryプロパティ (ホストは Program.cs で app.Services から設定。静的に作るので ctor では渡せない)。IndexReader の表名はスキーマ付きを . で分けて引用。保存時の埋め込みはSaveEmbeddingTimeout(既定 15 秒) で打ち切り、タイムアウト・プロバイダの中断例外も「文章だけ保存 + 警告」(8 回目レビュー: 以前は中断例外を投げ直して保存が失敗していた)。保存時の索引付けはInterceptor(上記)。埋め込み失敗 / 未設定は Vector null + 警告ログ。索引付けは DB を選ばない。★16 回目:ReadForIndexAsyncはリンク・モジュール連携 SelectField の表示名をFillLinkDisplayTextsAsyncで埋める (内部読みGetWithOwnedRecordsAsyncは DisplayText を埋めない = 0.20 のクライアント生成では入っていた得意先名が 0.22 の保存時索引・再索引で抜けていた。画面の LinkField と同じくリンク先を ValueVariable = 値で引く。権限は通さない)。internalReindexAsync(moduleDataIO, db, moduleName, missingOnly, progress, pageSize, ct)=moduleDataIO.CreateSystemAccess()(メソッドの先頭で 1 回作る) のGetListAsync(権限を通さない内部読み = 誰が起こしても同じ文章。8 回目レビュー) でページ読み→ページ分の文章を 1 回でまとめて埋め込み→同じ口のWriteWithoutSideEffectsAsync(書き込み専用列だけ。false = 消えた行は飛ばす。論理削除行は出ない。埋め込み失敗は例外)。missingOnly はSemanticSearchIndexReader.ReadIndexedIdsAsync(ベクトル列 not null の Id を生 SELECT) で索引済みを飛ばす。★2026-10-15: missingOnly はベクトルの無い行の文章をそのまま埋め込む (行を読み直さない・リンクを引き直さない = 保存時の埋め込み失敗の穴埋めが速い)。文章の無い行 (一括取込) だけ行から作る。ReadUnembeddedIdsAsync(先にベクトル無し・文章ありの Id を DB の値の型のまま控える) +ReadUnembeddedTextsAsync(ページごとに書く直前にその Id で文章を IN で読む = 全行の文章をメモリに持たない・再索引中に保存されてベクトルが付いた行は行から作る)。ベクトルのあるフィールドは触らない。プロバイダ無しなら文章のある行には何もしない。全行の Reindex は今までどおり行から作り直す。SemanticSearchIndexReader= 書き込み専用列を IDbAccessor の生 SELECT で読む (LoginAccountStore と同じ割り切り) - ★再索引はフィールドのスクリプト (2026-09-24 ユーザー決定「ボタンのモードよりフィールドのスクリプト。権限制御もできる」):
Fields/SemanticSearchFieldにReindex()/ReindexMissing()/CancelReindex()とIsReindexing/ReindexProcessed/ReindexTotal/ReindexError、デザインにOnReindexCompleted(ScriptEvent・引数なし・CheckFieldFunctionExistence)。クライアントは AIChatField と同じ形: 共通の API の Reindex / Status / Cancel (専用ルートは静的EndPoint= 互換) + ホストフックReindexCoreAsync、開始→requestId→Status ポーリング (1 秒→2.5 秒・最大 2 時間)→Module.ExecuteScriptAsync(OnReindexCompleted)。DTO はSemanticSearch/SemanticSearchReindex{Request,Response,StatusResponse}(Status は AIChatJobStatus の値を共用)。サーバーはSemanticSearchServiceのStartReindexAsync(owner, request, authorizeWith, openScope)/GetReindexStatus/CancelReindex(AIChatService と同形。= FieldApiAuthorization で入口検査→Task.Run。同じモジュールの走行中ジョブには合流 (Watchers)。SemanticSearchReindexScope(ModuleDataIO, IDbAccessor, owner)= バックグラウンド用に host が開く実行ユーザーの ModuleDataIO+DB。Example はDataService(string userId)で HttpContext 無しでもユーザー固定)。ホスト結線 = ExtrasApiTable のSemanticSearchApi(SemanticSearchTable.Service, userId => new SemanticSearchReindexScope(...))(Example はSemanticSearchIndex.Service)。テスト =Test/AI/SemanticSearchReindexTest(SQLite・まとめ埋め込みの回数 / missingOnly / 進捗 / 合流と中断 / 入口検査) - ★埋め込みは IEmbeddingProvider (2026-09-24 ユーザー決定「メールみたいに切り替えたい・固定ライブラリに引っ張られるな」): Extras.Server
AI/Embedding/にIEmbeddingProvider { ModelId; Dimensions; EmbedAsync(texts) }と、プロバイダ別の設定+実装AzureOpenAIEmbeddingSettings/Provider(Azure SDK) /EmbeddingGeneratorProvider(M.E.AI IEmbeddingGenerator のアダプタ)。★OpenAI / Ollama の実装は書いたが実 API 未検証のためユーザー指示で削除 (2026-09-24「一旦 AzureOpenAI だけでいい」)。必要になれば OpenAIEmbeddingCalls を共有して OpenAI、HttpClient で /api/embed を叩いて Ollama を足す。OpenAI 系はOpenAIEmbeddingCallsで 2048 件ずつ・Index で対応づけ・Dimensions>0 なら縮小指定。SemanticSearchService はFunc<IEmbeddingProvider?>を取り (RawDataAccessAgent はプロバイダを直接受けずSemanticSearchServiceを受けてGetProviderを SemanticSearchToolSet に渡す。null なら search_records は出ない)、Dimensions 申告と返ったベクトル長の不一致は例外 (CheckDimensions)。ホストは対応表 (Starter はSemanticSearchTable.CreateEmbeddingProvider、Example はEmbeddingProviderTable。メールの MailSenderTable と同形) を appsettings のSemanticSearch.EmbeddingProviderの呼び名で引く (Example: SystemConfig.SemanticSearch / AzureOpenAIEmbedding セクション。AISettings と同じく appsettings.Development.json 側)。AISettings.EmbeddingModelとAzureOpenAIClients.EmbeddingGeneratorFactoryは削除。テスト =Test/AI/EmbeddingProviderTest(設定必須・アダプタ) +AzureOpenAIEmbeddingProviderRealTest([Explicit]・次元縮小 256 も確認)。FakeEmbeddingGenerator →FakeEmbeddingProvider - キーワード検索 (0.21.0・2026-10-11 ユーザー決定「全文検索フィールドは作らない。原文を持っているのだからオプションで LIKE」):
SemanticMatchCondition.Keywords(空白区切り) = 文章の列 (DbColumnText) への語ごとのlike @p escape @eの AND (SemanticSearchFieldDesign.CreateWhere。ベクトルと両方あれば AND・キーワードだけならCreateOrderByは null)。語の切り方はSemanticSearch/SemanticSearchKeywords(internal)、語ごとの LIKE は本体のSqlConditionContext.Like(core 1.3.49。パターンとエスケープ文字をパラメータで渡す・SQL Server は[も文字扱い = 本体の Like と同じ規則)。デザインKeywordSearch(bool。★2026-10-12 一度 enumSearchBoxes(KeywordOnly = 文章の列だけの構成) にしたが「ややこしすぎる」で bool に戻し、列は常に 3 つ必須) = 検索レイアウトにキーワード欄も出す (SemanticSearchSearchComponent。キーワード欄はdata-role="semantic-search-keyword")。SemanticSearchField.KeywordText/SetKeywordTextAsync。GetMatchCondition は文章とキーワードを 1 つの SemanticMatchCondition に (文章が無ければ MaxDistance も付けない = 埋め込みを呼ばせない。Interceptor の前処理は Text が空なら触らない)。AI:ModuleDataAccessToolSet.ToConditionが SemanticSearchFieldDesign へのLikeを Keywords にする (他の比較はエラー)。search_records の条件の順は問わない (core 1.3.49 は同じ項目の条件のうち並べる式を返した最初のもので並べる)。★権限の仕様は 2 文だけ (2026-10-12 ユーザー決定「権限の仕様がややこしすぎる。スパッと説明して納得あって十分に使える仕様」): 検索できるかはこのフィールド自身の読み取り権限 (他のフィールドと同じ)。文章の列には SourceFields の中身がそのまま入るので、読ませたくない項目は SourceFields に入れない (入れるならこのフィールドにも同じ読み取り条件)。設計チェックはSemanticSearchFieldDesign:4= 対象フィールドに PermissionField の読み取り条件があるのにこのフィールドに無い (文言「{0} を SourceFields から外すか、{1} にも読み取り条件を付けてください (外したら Reindex())」) と:5(10 回目で追加) = 対象フィールドの読み取り条件が行の値で決まる (このフィールドの条件では代わりにならない → 外して Reindex())。条件の厳しさは見ない = 設計者の責任。「条件あり」は葉のあるものだけ (本体の IsEmpty と同じ)。Targetsの除外モードは本体と同じく Id / 楽観ロックを対象外。やめたもの: リンク先・タグ付けモジュールの UserReadCondition の指摘 (:5。LinkField を読める人には表示名が見えているので LinkField 自体の読み取り条件で足りる =ISemanticSearchTextSource.GetSemanticTextModuleNames/SemanticSearchText.ReferencedModuleNamesも削除)、「同じかそれより厳しい条件」「制限の違う項目が複数なら外せ」の説明。本体PermissionFieldDesign:4(行データを参照する読み取り条件を付けると検索は拒否される。ExecuteSql 向け:3と同じ型だが向きは逆 =:3は素通し・検索は拒否。6 回目レビューで「適用されない」の文言を直した) は本体の一般則として残す。権限の 2 文目は「同じ読み取り条件 (CurrentUser で決まるもの)。行の値で決まる条件の項目は SourceFields から外す」。ランタイムの権限は意味検索のときから変えていない (検索条件の権限判定は静的AuthorizationControl.GetFieldPermissionsStatic)。GetInstructions / DesignDescriber に案内。RawDataAccess (SemanticSearchToolSet.GetInstructions) には「語そのものは文章の列に LIKE」を列名つきで案内 (2026-10-11 ユーザー「足しといて」。IndexedModules= 3 列ありで接続が引けるモジュール全部に出す = プロバイダ無し・対応しない DB でも。search_records /{embed:}はSearchableModules= その部分集合のときだけ)。正規化 (NFKC 等) はしない = 一致は列の照合順序次第。テスト:SemanticSearchKeywordsTest/SemanticSearchKeywordDbTest(SQLite 実検索) / ConditionTest / ModuleDataAccessToolSetDbTest - デザインチェック:
SemanticSearchFieldDesign:1= 3 列必須。:2は欠番 (対象の一部だけ読み込む詳細レイアウトの指摘。サーバーが保存した行から作るようになり不要)。:3= 検索欄が一覧に効かない構成 (KeywordSearchが true なら出さない = キーワードだけで使う構成に直す手が無い。12 回目)。:4/:5= 上記の権限 (:5は同じ規則がこのフィールドも縛るなら対象外 = 本体 :4 が拒否で守る)。列存在 / SourceFields 存在もチェック。DB 種別 (対応 DB か) は DesignCheckContext に無いのでチェックしない (実行時に対象外になるだけ) - テスト:
Test/AI/(FakeEmbeddingProvider = 2-gram ハッシュ 128 次元 / SemanticSearchTextTest / SemanticSearchIndexingDbTest (SQLite・保存後の行からの索引付け (読めない人の更新も含む)・一括 INSERT は空のまま・旧 ApplyAsync は何もしない・デザインチェック) / SemanticSearchToolSetTest (接続せず判定と説明) / SemanticSearchDbSearchTest (SQL 文字列) / SemanticSearchVectorDbTest は [Explicit]・実 DB (FakeEmbedding で 保存→再索引→search_records→{embed:} SQL 実行。環境変数 SEMANTIC_SEARCH_PG_CONNECTION = pgvector 済み PG / SEMANTIC_SEARCH_MSSQL_CONNECTION = SQL Server 2025。2026-09-23 に Docker の pgvector/pgvector:pg16 と mcr.microsoft.com/mssql/server:2025-latest で両方緑 = 生成列 ::vector / VECTOR_DISTANCE / cast(is_deleted as integer) の構文確認済) / SemanticSearchRealAITest は [Explicit]・実 Azure OpenAI + pgvector PG (環境変数 AZURE_OPENAI_* と SEMANTIC_SEARCH_PG_CONNECTION)) - ファイル:
Designs/SemanticSearchFieldDesign.cs,Data/SemanticSearchFieldData.cs,Fields/SemanticSearchField.cs,Components/SemanticSearchFieldComponent.razor(空),SemanticSearch/SemanticSearchText.cs, ServerAI/SemanticSearch/SemanticSearchService.cs/SemanticSearchInterceptor.cs
- 状態: 実装済み (docs/EditHistory.md, Extras.Designer FieldDocs/EditHistoryFieldDesign.md + EditHistoryContractFieldDesign.md + EditHistoryUndeleteButtonFieldDesign.md)。Example とテンプレート (Starter 4 ホスト) の CustomizedModuleDataIO に結線済み。仕様の正本は docs/EditHistory.md (この節は実装の地図と、決めた理由)
- 位置づけ: 利用者向けの変更履歴で、監査ログではない (監査ログ = 失敗や拒否・閲覧・ログインも含む記録は別機能として作る。未着手)。受け取った送信をそのまま残す役割 (Command) は一度入れて削除した (差分で足りる・スナップショットと二重)
- 構図: 承認フローと同じ契約フィールド方式。対象モジュール側 =
EditHistoryField(「履歴を取る」マーカー + 版一覧 UI。HistoryModuleName/LayoutName/PageSize/ExcludedOwnedRecords/IndividuallyRecordedOwnedRecords)、履歴モジュール側 =EditHistoryContractField(役割 ModuleName / DataId / ChangeType / Snapshot 必須、UserId / DateTime 任意) とEditHistoryUndeleteButtonField(削除の版からの復活)。EditHistoryChangeTypeは[DesignEnum](Add / Update / Delete / Restore。メンバー名 = DB 保存値なので変えない)。履歴行から対象レコードを開くのは本体の AnchorTagField (ModuleVariable / IdVariable) = 専用フィールドは作らない - 本体 (core 1.3.37) との分担: 本体は部品、何をどう記録・表示するかは Extras。Extras はフィールドの型を見ない (一覧・埋め込み・Gantt を型で分岐しない。従属レコードの仕組みは本体の interface)
- 従属レコードの宣言:
IOwnedRecordsFieldDesign.GetOwnedRecords(IModuleDesigns modules)(core 1.3.45 から全デザインを受ける。結び付きを相手のモジュールの契約から組み立てるフィールドのため) →OwnedRecordsDesign{Name, Condition, HoldsAllRecords, References, IsReferencedByOwner, DeleteTogether}。DeleteTogether (core 1.3.55) = 親の削除で本体が一緒に消す宣言 (一覧の DeleteTogether と同じ経路。TagField が使う)。References = 行の項目のうち他の宣言の行を Id で指すもの (Gantt の依存関係 → タスク)。IsReferencedByOwner = 親が行を参照する宣言 (埋め込み。宣言が明示する。条件の形から推測しない)。本体のListFieldDesignBase(DeleteTogether か CanCreate / CanUpdate / CanDelete のどれか = 親の保存に乗る一覧) とModuleFieldDesign(DbColumn と ModuleName があるとき) が実装。Extras の Gantt (タスク +フィールド名:Dependencies) / Calendar / TaskBoard / MarkerList も実装 - 従属レコードのデータ:
IOwnedRecordsData(GetOwnedRows / SetOwnedRows / RemoveOwnedRows。ListFieldData と ModuleFieldData が実装) +ModuleData.GetOwnedRows(name)。埋め込みモジュールの版の形は ModuleFieldData (Id + Data に子の内容) - 従属レコードのランタイム:
IOwnedRecordsField= 行の出し入れと表示だけの 6 メンバー (AllowLoad/LoadOwnedRecordsAsync(全部の行を返す) /AddOwnedRecordAsync(name, id)/RemoveOwnedRecordAsync/RefreshOwnedRecordsAsync/ShowOwnedRecordsAsync(name, IReadOnlyList<OwnedRecordRow>))。足した行の内容は連動なし (SetDataWithoutInteractionAsync。行ごとのリンク先の問い合わせを起こさない = 本体 InsertRowsAsync と同じ考え)、今ある行は連動あり。本体の ListField と ModuleField、Extras の 4 フィールドが明示的実装で持つ (フィールドの公開メンバー・スクリプトには出ない)。★差し替えの手順 (行 Id の突き合わせ・内容の反映・論理削除の行の Id を保った復活・余った行の削除・孫への再帰・References の付け替え・指される宣言を先に処理) は本体Module.ApplyRecordAsyncの 1 か所 (2026-09-29 に各フィールドの実装から移した。理由 = 同じ手順が 3 か所にあり、Gantt の依存関係はタスクの差し替えが先に済んでいる前提という隠れた順序があった)。表示専用のモジュールは本体FieldBase.CreateModuleForShowAsync(data, layoutName, toRows)(AllowLoad を止める → 内容を入れる → 従属を見せる、を 1 回で) - IO の拡張点:
IModuleDataIOInterceptor(SubmitAsync(io, context, transactionData, next) = 保存を包む / GetListAsync = 読み出しの後処理。第 1 引数はModuleDataIO io) とModuleDataIOSystemAccess(アプリ自身 (システム) の権限で読む・書く操作 = ユーザーの認可を通さない: GetWithOwnedRecordsAsync / GetListAsync / AddAsync / UpdateAsync / WriteWithoutSideEffectsAsync)。システムの操作はio.CreateSystemAccess()(サーバーの機能はModuleDataIO.CreateSystemAccess()) で作る。保存の状態 (TransactionId / ChangedRecords / KeepFilesOfDeletedRecords) は SubmitAsync の引数のSubmitContext(保存の外には無い)。読む人の権限に落とす RestrictToReadableAsync・取り消しの権限 CanUndeleteAsync・現在のユーザー・アプリアクセス・フィールドの入口検査 (GetCurrentUserAsync / CheckAppAccessAsync / CheckFieldUserRead(Write)Async) はio.AuthorizationChecker、ユーザーの権限で行う取り消し (UndeleteAsync / UndeleteTogetherAsync) はioの public (core 1.3.50) - 判定の部品:
ModuleDesign.UsesLogicalDelete()/HasUserReadPermission(services)/CanUndeleteByUser(services)、IdFieldData.IsTemporaryId、OwnedRecordsExtensions.IsSystemOrLinkedField
- 従属レコードの宣言:
- 結線: ホストは
AddInterceptor(new EditHistoryRecorder(designData))の 1 行 (logErrorは任意)。保存 (記録・復活) と読み出し (Snapshot を読む人の権限に落とす) を 1 つの実装が受けるので、片方だけ結線した状態が存在しない。ホストに追加のメソッドは要らない - 記録 (Extras.Server
EditHistory/EditHistoryRecorder): 流れは「履歴モジュールへの書き込みの拒否 → 削除の取り消し (EditHistoryUndeleter) → 計画 (Plan) を立てる → next() = 本体の保存 → 保存後の内容を内部読みして履歴モジュールへ内部 add」。ルート 1 件 = 履歴 1 行- 削除は next() の前に削除前の内容を読む。作成・更新は next() の後に結果の DestinationId で読む
- 内部読み = 操作ユーザーの権限に関係なく全列・全従属 (書いた人の権限で版が欠けると、権限の弱い人の版が列を欠いて記録され、復元でその列を空にしてしまう)。
EditHistorySnapshot.FillNullsが NULL の列を null 値で埋める (本体の読み出しは NULL の列を入れないので、無いと「その版では空だった」が残らない) - 履歴対象モジュールの投入は
NoTemporaryIdResolution = falseにして本体の一括 INSERT 経路を使わせない (採番 Id が返らないため)。Id 空の新規行 (一括取込) はAssignTemporaryIdToRootAddで仮 Id を付けて採番 Id を得る。作成 / 更新の判定は Add 一覧にルートがあるか (IsRootAdd) - 版にしない送信: 何も保存しない送信 (
IsEmpty) / 含めない従属レコードの行だけの送信 (OnlyNotIncludedRows) / ExecuteSqlField の Standalone で前後のスナップショットが同じ (IsStandalone+Plan.Before) - 保存後の Id が取れない・保存後のレコードが読めないときは版を作らず保存は成功させ、logError に出すだけ (仕様として割り切り。テンプレートは logError を渡していないので無音)
- 履歴の書き込みの失敗・設計不備 (履歴モジュール / 契約なし) は結果の ExceptionMessage = ロールバック
- Id を変えた更新は
EditHistoryUndeleter.RelinkVersionsAsyncで旧 Id の版を新 Id に付け替え - 変更日時は履歴モジュールの DateTime 役割フィールドの
SaveAsUtcに従う (本体の CreatedAt と同じ。サーバーローカル固定にしない)。ミリ秒まで - 版番号は保存しない (記録側が採番すると、履歴を読めないユーザーの保存で狂う)
- 添付: next() の前に
context.KeepFilesOfDeletedRecords(submitData)(履歴を持つモジュールのレコードに触る送信)。本体のFileFieldDataIOが削除・差し替えで外れる添付を一時領域へ送らない
- 読み出し (
EditHistoryRecorder.GetListAsync): 履歴モジュールの行の Snapshot をio.AuthorizationChecker.RestrictToReadableAsyncで読む人の権限に落とす (落とせない・壊れている・対象を読めないなら空)。Snapshot を検索条件・並びに使った読み出しは例外で拒否 (生の内容で絞ると伏せた値を推測できる)。行条件は「レコード自身の列は版の内容・リンク越しは今のリンク先」(通常の読み出しと判定基準を揃える。リンク値を版に凍結する案は採らなかった)。判定できない条件 (版に無い一覧の 1:N) は読めない扱い - 復活 (Extras.Server
EditHistory/EditHistoryUndeleter・internal): クライアントはEditHistoryUndeleteData{HistoryModuleName, HistoryRowId, RestoreWholeRecord}を ModuleSubmitData.ExtendedData に載せるだけで、サーバーが版のスナップショットから戻す (版は送信の対象レコードのものに限る)- 復活ボタン (
RestoreWholeRecord = true): 最新の版が Delete で、今そのレコードが無いときだけ。論理削除 =UndeleteAsync(ユーザー権限) + 子はUndeleteTogetherAsync(子の CanDelete だけ = DeleteTogether の逆)。物理削除 =RecreateAsync(手入力 Id / 複合 Id は元の Id、自動採番は新 Id +RelinkVersionsAsync。行の References は作り直した行の Id に付け替え =ResolveReferences・指される宣言を先に作るOrderByReferences)。束縛の向きはGetBinding(子側が Id なら親が子を参照 = 埋め込み: 子を先に用意して親の参照を付ける)。版は常に Restore - この版に置き換える (
RestoreWholeRecord = false): 版をAuthorizationChecker.RestrictToReadableAsyncで見える内容に落としてから、「今は削除中」の論理削除の行だけUndeleteAsync。戻せなければ例外 = 保存失敗 - 元 Id の再利用 (自動採番) は DB 依存 (IDENTITY_INSERT 権限・identity ALWAYS) なので採らない。物理削除の復活を機能ごと外す案は「端の仕様で利便性を削らない・条件を絞って制限を付ける」で不採用
- 復活ボタン (
- 従属レコードの扱い (
EditHistory/EditHistoryPolicy): 含める (既定) / 除外 / 行ごと。パス = 宣言の Name をドット区切り ("Items.Details")。IsIncluded / IsIndividual / Walk / NotIncludedModules / Strip。判断は Extras だけで行い、本体はGetWithOwnedRecordsAsync(module, id, includeOwnedRecords)のパス選別を提供するだけ (DeleteTogether を履歴のスイッチに借りない)- 行ごと: 親の送信の Add / Update / Delete のうち行モジュールの分を
Plan{RowTempId | RowId}にして行モジュールの契約へ 1 行 1 版。親削除で消える行の Delete 版はIndividuallyRecordedDeletePlansAsync - 親に EditHistoryField が無いときは、自分の EditHistoryField を持つ従属モジュールを自動で行ごと (
IndividualTargets(module, field: null)) Walkは同じモジュールに複数の経路で届くとき、その先に除外・行ごとの指定が無ければ中を 1 回だけ列挙する (全経路を列挙すると、明細で互いに繋がった設計で経路の数だけ膨らむ)。Recorder は設計に編集履歴が 1 つも無ければ素通し (_usesHistory)、行ごとの記録先はモジュールごとに 1 回だけ求めるWalkの循環停止はモジュール単位 (設計の列挙)。本体の読み出しの循環停止はレコード単位 (自己参照の従属を全深さ読む)
- 行ごと: 親の送信の Add / Update / Delete のうち行モジュールの分を
- 閲覧 (
Fields/EditHistoryField): 初期化は件数だけ (LoadCountAsync= Id だけの GetList)、ExpandAsyncで版を読む (展開状態はフィールドに残る)。版を引く条件はEditHistoryContracts.VersionsConditionに一本化 (Field / UndeleteButton / サーバーが同じ並びを使う。食い違うと「最新の版か」の判定が割れる)。ページの次の 1 行も同時に読み、ページ末尾の版の差分の元にする。版番号の基準は最初のページの総数で固定 (_numberingTotal)。読み込み中の読み直しは_reloadPending。自動保存は再初期化せずAcceptChangesだけ呼ぶので、そこで読み直す (ReloadAfterSubmit)。履歴モジュールを読めない人には要求しない (HasUserReadPermission) - 差分 (
EditHistory/EditHistoryDiff・EditHistoryValues): 値は文字列にできる型だけ「旧 → 新」(IsTextSupported)、他は JSON 比較で有無だけ。比較は全文・表示だけ 300 文字に切る。従属レコードは行 Id で突き合わせ、行は行番号で示す (行を代表する項目を推測しない)。親へのバインド条件のフィールドは行の値に出さない。項目の閲覧権限はCanRead(従属の行は_ownedModules= 空のモジュールを作って引く) - この版を表示: 本体
CreateModuleForShowAsync(snapshot, LayoutName, (declared, rows) => OwnedRecordsDisplay.Build(rows, change))(追加 / 変更 + セル / 削除行の差し込み。孫も) → SubmitButtonField と EditHistoryField を非表示 →ShowDialogAsync。強調 CSS はダイアログがコンポーネントの外に描画されるのでwwwroot/edit-history.cssをedit-history-interop.js(ensureStyles) でページに入れる (scoped CSS では届かない) - この版に置き換える (
EditHistory/EditHistoryRestorer): 添付ファイルとIEditHistoryExcludedFieldDesignの項目を外して本体Module.ApplyRecordAsync。論理削除の行を Id 付きで戻したらonRevive→EditHistoryField.AddPendingUndelete→GetSubmitDataの ExtendedData に同梱。反映数 0 ならメッセージだけ - 対象外の宣言:
Designs/IEditHistoryExcludedFieldDesign(目印)。ApprovalFlowFieldDesign が実装 (承認 FK はサーバーが保存の後に書くので、差分に出すと幽霊差分になり、戻すと画面の承認状態だけ過去に戻る)。Diff と Restorer は型名でなく目印で判定 - Extras の 4 フィールドの部品:
Fields/OwnedRecordModules(AddAsync = 行を 1 つ足す (新しい行 / Id のある行。どちらも親への束縛の値を入れる) / CreateForShowAsync = 表示専用の行モジュール /ShownClassName()= 表示専用の行の項目に出すクラス)。履歴に依存しない。★項目 (バー・予定・カード・マーカー) に行モジュールのClassNameを出さない (レイアウトのデザインのクラスが入っていて、通常の表示の見た目が変わる)。Gantt のAllowLoad = falseでの絞り直しは表示専用のときだけ (_isShowingOwnedRecords)。Gantt / Calendar / TaskBoard / MarkerList はLoadOwnedRecordsAsyncで全件を読み直す (表示範囲しか持っていないため)。Fields/UnsavedRecordGuard= 未保存の親 (Module.IsNewData) では子を読みに行かない (本体の仮 Id ガードとは別。手入力 Id の新規は Id が空で、本体のガードでは止まらない) - 設計チェック:
EditHistoryFieldDesign:1〜:11/EditHistoryContractFieldDesign:1〜:3/EditHistoryUndeleteButtonFieldDesign:1(一覧は docs/EditHistory.md「設計チェック」)。番号は固定 (追加は末尾・欠番は再利用しない) - セットアップ (Extras.Designer
Setup/EditHistorySetupService/EditHistorySetupWindow/ CLIedit-history-setup): テンプレート = Example のEditHistory.mod.jsonを埋め込み、名前・テーブル名・データソース・ユーザーモジュールを差し替える。enumEditHistoryTargetModuleは空で生成 - SeleniumDrivers:
EditHistoryFieldDriver/EditHistoryVersionDriver/EditHistoryVersionDialogDriver/EditHistoryUndeleteButtonFieldDriver(1 ファイルEditHistoryFieldDriver.cs)。Versions/FindVersion/LoadMoreは未展開なら自動で開く。版の中に details を足すときは summary の指定を直下限定 (:scope > summary) に保つ - テスト:
Test/EditHistory/*(Diff / FieldHarness / RecorderDb / PolicyDb / ModuleField{Db,Diff,Restore,Show} / Gantt{Restore,Dependencies} / OwnedFields{Restore,Show} / OwnedDisplay / ShowVersionFlow / ApprovalField / AutoSubmitReload)、Test/DesignCheck/EditHistory{,Policy}DesignCheckTest、Test/Approval/ApprovalEditHistoryDbTest、Test/Setup/EditHistorySetupServiceTest、Test/Fields/UnsavedRecordGuardTest。e2e は tfs の SeleniumTestScenario/Normal/Extras/EditHistory*Test(デザイン = SeleniumTestCookie のModules/EditHistory)- SQLite のテスト用テーブルは日時列を DATETIME / DATE で宣言する (TEXT だと書式がカルチャで混ざり、日時順が崩れる)
- ExecuteSqlField を持つモジュールの送信は CurrentEditingData が要る (無いと内部エラー)
- Example のテスト用モジュール:
Order+OrderItem(明細・論理削除)、Project+ProjectTask(Gantt)、Plan+PlanEvent/PlanCard/PlanMarker(Calendar / TaskBoard / MarkerList)、履歴モジュールEditHistory+ enumEditHistoryTargetModule。テーブルはローカル SQLite。承認系のモジュールは他の確認にも使うので履歴の検証には使わない - 未対応: 「この版に置き換える」での添付ファイルの復元 (実体は残っているので、Restorer が FileFieldData を外さなければ戻せる。要望が出たら) / 洗い替え (ReplaceMode) の一覧は行 Id が毎回変わるので差分が「削除 + 追加」に見える (記録・復元は正しい) / 自動保存の版をまとめる機能 / 保持期間の自動削除
- 状態: 実装済み (docs/JobTriggerField.md)。Extras 0.23.0 (2026-10-21。本体 1.3.52 から移動。本体は 1.3.53 で削除。1.3.53 で残した印
IExecutionFieldDesignは 1.3.54 で削除 = 本体はジョブを一切知らない) - 構図:
Designs/JobTriggerFieldDesign(JobName / ShowButton / ButtonText / UseSubmitData / IsAsynchronous (起動したらすぐ戻り「開始しました」を通知。Run() は起動の成否。結果は待たない) / AllowsConcurrentRuns (既定 false。同じ JobName が実行中なら起動しない。判定はサーバーがジョブ名で)。設計チェック :1 ジョブ名必須 / :2 モジュールの DataRead/WriteCondition は効かない / :3 このフィールドを対象にした PermissionField の行条件は効かない。行条件の判定と対象判定はDesigns/MatchConditionDesignExtensions(SemanticSearchFieldDesign と共用)) /Fields/JobTriggerField(Run()= 入力チェック →{"data","submit"}の JSON → 送信 → 通知。送信はRunCoreAsyncの差し替え → 静的EndPoint(専用ルート。互換) への POST → 共通の API の Job.Run の順。どれも無ければ「このアプリではジョブを実行できません」。非同期は応答のJobIdをJobIds(終わりを確かめていないもの・起動順。スクリプトで扱えるよう string[] の写し。JobIdはその最後 = LastOrDefault、無ければ空) に持つだけで、フィールドは自動で問い合わせない。スクリプトがGetJobStatus(jobId)(C# はGetJobStatusAsync(string jobId = "")。空なら JobIds の先頭 = FirstOrDefault、それも無ければ問い合わせずに null) で 1 回ずつ問い合わせる = Job.Status (専用ルートならGET {EndPoint}/{jobId}。その 1 回だけトーストを抑える) か差し替えGetJobStatusCoreAsync(RunCoreAsync があって無ければ null)。終わっていれば JobIds から外す。分からない (404・通信失敗・問い合わせるジョブが無い・デザインモード) は null (404 = サーバーが忘れた、と差し替えの null は JobIds からも外す。通信失敗は残す)。戻り値のJobStatusResponseはExtrasClientInitializer.Initialize(app)で AddType (スクリプトのメソッドは戻り値の型が登録されていないと見えない)) /Jobs/JobRunRequest・JobRunResponse (Started + JobId)・JobStatusResponse (Status = enumJobs/JobStatusRunning/Done/Error (JSON は core の string enum 変換で名前)・Message・Error。IsRunning/Succeeded の bool は置かない (enum で足りる)。AIChatJobStatus には依存しない。enum は AddType でスクリプトに公開)(DTO。Extras.Server の JobApi 側も同じ型。状態の問い合わせはJobs/JobStatusRequest) /Components/JobTriggerFieldComponent(ButtonField と同じ ViewOnly 制御) - テスト:
Test/Fields/JobTriggerFieldTest(RunCoreAsync / GetJobStatusCoreAsync の偽ホスト + 記録する IUIService) /Test/DesignCheck/JobTriggerFieldDesignCheckTest
- 状態: 実装済み (docs/Jobs.md)。Extras.Server 0.23.0 (2026-10-21)
- 構図 (Extras.Server
Jobs/): ホストのアプリ自身を別プロセスでjob <ジョブ名> --stdinで起動する親JobProcess(static。RunAsync= 待つ (記録しない) /StartAsync= 起動してジョブ Id (GUID "N") を返す・失敗はLoggerFactoryにも Error。どちらもallowConcurrent(既定 true) が false で同じジョブ名 (大文字小文字無視) が実行中なら起動せず null (実行中の数は lock した Dictionary で数える。同期・非同期とも・起動の失敗でも減らす。プロセス内だけ)。GetStatusAsync(jobId, userId)= 非同期のジョブの状態 (知らない Id・別ユーザー・Cache未設定は null)。状態はCache(IDistributedCache。ホストが起動時に設定) にキー"Job:" + jobId・JSON (ユーザー Id・Status・Message・Error) で置く: 起動 (TryEnter → Launch) を同期的に済ませてから running を期限なしで書き、裏のWatchAsyncが終了で done / error に書き換えてFinishedRetention(既定 30 分) の AbsoluteExpiration。書き込みの失敗は LogFailure に流して黙る。テンプレはメモリキャッシュ = 再起動で消え、複数インスタンスでは共有キャッシュ (LoginLockout と同じ)。途中で落ちたジョブは running のまま残る。HostExecutable= EntryDll の隣の apphost → single-file なら ProcessPath → 無ければDotnetPath+EntryDll) と、子側JobMode.RunAsync(args, jobNames, runJob)(UTF-8・--stdinを標準入力の内容に置換・終了コード 0/1/2・最後の行 = 成功の文言・標準エラー 1 行目 = 例外のメッセージ)、結果JobResult(Succeeded / Message / ErrorMessage / ExitCode / Output / Error)。親子の約束はこの 3 つに閉じる。ジョブの型 (IJob)・DataService の組み立てはホストテンプレ (Starter CookieServer/Jobs/JobRunner.cs) = 利用者が触る側 - 使う側: Extras の
JobTriggerField(画面のボタン → 共通の API の Job.Run → Extras.Server のJobApi(ホストは ExtrasApiTable にnew JobApi(JobRunner.Names)) →JobProcess。非同期の状態は Job.Status →JobProcess.GetStatusAsync。null なら応答なし) と、コマンドライン / タスクスケジューラ / Azure WebJobs (Program.csのjob分岐 →JobMode) - テスト:
Test/Jobs/(JobMode 同一プロセス・JobResult の行の取り出し・JobProcessTest = テスト用の最小ホストCodeer.LowCode.Blazor.Extras.Test.JobHostを dotnet と apphost の両方で起動する往復。状態の置き場は時計 (ISystemClock) を差し替えた MemoryDistributedCache。Test.csproj の ProjectReference でテストの bin に出力が写る)
- 状態: 実装済み (docs/AuditLog.md)。Extras.Server 0.17.0
- 位置づけ: 「いつ・誰が・どこから・何に・何をして・どうなったか」を WebAPI ごとに記録する (状態を変える操作は試行+結果の 2 行)。トランザクションは業務と別 (CLB は複数 DB なので合わせられない。失敗の記録がロールバックに巻き込まれない利点)。J-SOX / ISMS の監査証跡向け。編集履歴とは別 (編集履歴 = 業務データの変更、監査ログ = 失敗・拒否・参照・ログインも含むシステム操作の記録)。値は記録しない。ハッシュチェーンは持たない (権限分離 + 外部転送で足りる)
- 構図 (Extras.Server
AuditLog/): 保存する部品だけを持ち、何を記録するかは呼び出し側 (WebAPI の入口) が決める。AuditEvent(レコード) /AuditLogger(シングルトン。記録の規則は固定 = 分類あり (Other 以外) は全部・Other は失敗と拒否だけ・試行はHasAttemptの分類 (Authentication/DataWrite/Export/Admin) だけ、全IAuditSinkへ書く、Strict ならAuditLogException・BestEffort は Critical ログ) /DatabaseAuditSink(固定列・書き込みごとに専用の IDbAccessor・CreateTableSql) とFileAuditSink(JSON Linesaudit-{host}-{yyyyMMdd}.jsonl) /AuditLogMiddleware(UseRouting 直後・認可の前に置く = 認可ミドルウェアの 401/403 とアンチフォージェリの 400 が見える。MVC フィルタではこれが見えない。認証は自分で解決 = 既定スキームで AuthenticateAsync。分類は[Audit(AuditCategory)]、無ければ Other。二段記録 (2026-10-01 ユーザー決定「両方同期」): 操作の前に Result=Attempt の行 (AttemptCategories。既定 Authentication/DataWrite/Export/Admin。DataRead は付けない) → 書けなければ Strict で操作を実行しない、操作の後に結果の行。RequestId で結ぶ。後段の書き込みはResponse.OnStarting= レスポンス先頭の前。Kestrel/IIS は未開始扱いなので Strict の失敗は例外ハンドラの 500 になる。TestServer は開始済み扱いで例外がそのまま届く) /AuditContext(スコープ。コントローラが対象・業務上の失敗・ログイン時のユーザーを足す) /AuditLogHostedService(起動 = 有効設定を Detail に・停止。Host の Dispose だけでは StopAsync は走らない) /AuditIOInterceptor(IO インターセプタ。保存の合流点で対象を記録 + 監査テーブルへのモジュール保存を拒否。他のインターセプタより先 = 外側に登録) - 設定:
AuditLog(Enabled / FailureMode 既定 Strict だけ。★2026-10-02 ユーザー決定で Categories / AttemptCategories は削除 = 何を記録するかはコントローラの[Audit]が決める。★同日ユーザー「Id は残さない場合でもアクセスに来たログは残したい」で訂正: 一覧取得 (ModuleData.GetList) は常に[Audit(DataRead)]、AuditContext.AddRead(module, page, recordIds)= 既定 false はモジュール名の Read +Rows=だけ、true で行ごとの Id (テンプレ false / tfs Selenium サイト true)。RetentionDays / Purge も削除 = アプリは監査ログを消さない・IAuditSinkは WriteAsync だけ) 。出力先は同じセクションの入れ子Database(DataSourceName / Table) /File(Directory) (★2026-10-02 3 セクション → 1 つに統合)。有効なのに出力先が無ければ起動エラー。ホストはAuditSinkTable(FileStorageTable と同形) で IAuditSink の並びを組みAddAuditLog(settings, sinks)+UseAuditLog() - 結線 (Starter Cookie): Controller に
[Audit]、共通の API はハンドラのGetAuditCategory。ModuleData = list (行ごと Read) / submit / submit_by_file / bulk_submit = 対象 (行ごとの Add / Update / Delete と Id)・件数・結果の ExceptionMessage → Failure はAuditIOInterceptor→AuditContext.RecordSubmitAsync(internal) が保存の合流点で記録する (コントローラは包まない。対象は保存前の送信内容から、仮 Id は結果の TemporaryIdMap/DestinationId で採番 Id に。Id の無い新規行には仮 Id を付けて引く) / list_file・list_file_by_data (Export。行の Id と Rows) / upload (DataWrite) / download (Export。添付の持ち出しは一覧のファイル出力と同じ扱い)。Account = login (LoginName を Detail・ユーザー Id は Cookie 発行時だけ = 二要素待ちの Success 行は user_id 空・二要素のコード不一致は Deny)・login_ticket・logout (Authentication)。共通の API: Mail.Send / SendBulkSearch (Export)・Mail.Preview / PreviewBulkSearch (DataRead。一斉は描いた宛先の行ごと Read をMailPreviewBuilderが足す)・Job.Run (DataWrite)・Approval.Execute (DataWrite・結果の TargetId)・AIChat.Send (DataRead)・SemanticSearch.Reindex (Admin)・Totp.Reset (Authentication)・Excel.ConvertPdf (Export)・AITextAnalyze.AnalyzeFile / AnalyzeText (Other) - 外部ログインのコールバック: IdP からの戻り (
/signin-{name}) は認証ハンドラが処理しコントローラのアクションではない = ミドルウェアに見えないので、ExternalLoginAuthenticationがAuditLogger(DI にあれば) へ直接書く。Action =Account.ExternalLoginCallback、成立 = Success + ユーザー Id、未登録・クレーム不備・IdP 側の失敗 = Denied (Provider=...; Error=...)。Strict で書けなければサインインしない。ネイティブアプリの成立は login_ticket のアクションが記録する (コールバックでは書かない = 二重に数えない) - DB の時刻:
DatabaseAuditSinkは日時を Kind=Unspecified で渡す (Kind=Utc のままだと Npgsql が timestamptz で送り、timestamp 列へセッションのタイムゾーンに直して入る) - デザインの版 (
AuditEvent.DesignVersion/design_version列): App.zip の SHA-256。ホストがAddAuditLog(settings, sinks, designVersion)にFunc<HttpContext?, string>を渡す (HttpContext あり = そのリクエストが使う版、null = 今読み込んでいる版)。ミドルウェアは前段の前に版を取り、試行と結果の 2 行に同じ値を書く。ホスト (Starter Cookie) はRequestDesign(スコープ) でリクエストの最初に版を 1 つに固定し、コントローラはDataService.Design経由でその版だけを使う (AppDesign.Store(Extras.ServerDesign/DesignDataStore。版 =LoadedDesign.Version) から直接取らない。途中で差し替わった版が混ざるため)。プロセスの版が替わるとAuditLoggerが System のDesign.Loaded(Previous=前の版) を 1 回書く (インスタンスごと)。App.zip の保管と送信者の記録はシステム外 (デザイナの送信履歴フォルダDeployInfo.HistoryDirectoryが版の先頭 16 桁_UTC 日時_マシン名.zipで保存する。送信者は記録しない = なりすましを防げない自己申告は証拠にならない) - DB の DDL: MySQL の targets / detail は
longtext(text は 64KB で行の多い対象が入らない)。SQL Server は時刻が 1/300 秒に丸まる (Dapper が DateTime を datetime で送る。実害なし) - 監査の対象外 (2026-10-01 ユーザー決定): AI チャットの RawDataAccessAgent (execute_sql / search_records / {embed:}) は監査対象外。AI 用 DB ユーザーの生 SQL で行制限が効かず、ジョブ実行で読んだ行を Id で残せないため。★2026-10-02 ユーザー「使ったログは残る想定」で訂正: AIChat Send は
[Audit(DataRead)](3 ホスト) +AIChatService.StartAsyncがAuditContext.Currentに対象 (モジュール・AIChat:フィールド名) とAgent=名前((default) = 空) を足す = 誰がどの画面でどの Agent に聞いたかは残る。残らないのは発言と Agent が読んだ行だけ。AI チャットの「監査ログ」という言葉は「実行ログ (ILogger)」に改めた。方針: 監査が要る環境向けに、実行ユーザーの ModuleDataIO 経由で読む制御の効いた Agent を作る (search_records も SearchCondition + GetListAsync に。ジョブは Start 時点のユーザー・RequestId・版で AuditLogger に直書き) - 権限拒否は Denied (2026-10-02): 本体 1.3.38 の
LowCodeAccessDeniedException(権限で拒否する全箇所が投げる派生型。文言は同じ) をミドルウェア (例外 → Denied) とAuditContext.RecordSubmitAsync(catch → Deny + rethrow。本体SubmitWithoutInterceptorsAsyncはこの型だけインターセプタまで上げ、SubmitAsyncが同じ形の ExceptionMessage にする) が見る =result = 'Denied'で権限外アクセスの試行を文言に依存せず引ける。Extras.Server は core 1.3.38 を直接参照。★2026-10-13 (意味検索 18 回目):RecordSubmitAsyncは権限拒否以外の例外も catch → Fail + rethrow。内側のインターセプタ (編集履歴・意味検索) が投げた例外は本体SubmitAsyncが最外で ExceptionMessage (HTTP 200) にするのでミドルウェアには届かず、付けないと保存が巻き戻ったのに Success で残る - 対象と件数 (2026-10-01): 触ったレコードは経路によらず Id で残す。
AuditContext.Current(AsyncLocal。ミドルウェアが入れる。リクエスト外は null。リクエストから起動するジョブ (SemanticSearchService / AIChatService の Task.Run) はExecutionContext.SuppressFlow()で引き継がない = 応答済みのリクエストの記録に書き足さない) から、引数で持ち回れない処理 (インターセプタ・BulkFileTransfer・Mail) がAddTarget/AddCount(Detail 先頭に名前=件数) /AddNote(File=取込ファイルの SHA-256) を足す = 公開メソッドに監査用の引数を足さない。1 レコードの対象はAuditLogger.MaxTargetsPerRecord= 500 件までで、超えたら同じ RequestId の続きの行 (AuditResult.Continued。切り捨てない)。本体の一括 INSERT (BulkAddThreshold以上の純追加) は採番 Id を返さない → その Add は Id 無しでモジュールごとに 1 件 + 件数 (監査のために一括 INSERT を止めない。編集履歴と違い全モジュールが対象なので)。メールは送信元レコード + 件数、一斉送信は宛先の行を Id で (MailTo。アドレスは残さない)。SearchDelete (ListField の洗い替え) はモジュール名だけ (Id は本体の変更が要る) - テストの罠:
AuditContext.Current(AsyncLocal) を async の SetUp で入れてもテスト本体には伝わらない → 各テストの先頭で同期のヘルパーから入れる - セットアップ (Extras.Designer
Setup/AuditLogSetupService/AuditLogSetupWindow/ CLIaudit-log-setup): テンプレート = Example のAuditLog.mod.json(固定列の閲覧モジュール。日時は SaveAsUtc・分類/結果は候補付き Select・操作者はユーザーリンク・誰も書けない保護条件) を埋め込み、名前・テーブル名・データソース・ユーザーモジュールを差し替える。DDL はモジュールからではなくAuditLogTableDdl(Extras.Designer 内。DatabaseAuditSink.CreateTableSql/CreateIndexSqlの複製。Designer は Server を参照しないため。同一性はTest/Setup/AuditLogSetupServiceTestが 5 DB で検証) = テーブル +occurred_at_utcのインデックス。テーブルが既にあれば DDL を出さない。FieldDocs は無い (フィールドが無いのでフィールドカタログには載らない)。代わりにSpecDocs/AuditLog.mdをSpecDocCatalogに登録 (ExtrasSpecDocs。埋め込みCodeer.LowCode.Blazor.Extras.Designer.SpecDocs.*.md) し、ai-refresh が CCFD の_specs/AuditLog.mdに書き出す = CCFD はこれで「監査ログ = サーバー機能・自作しない・audit-log-setup で生成・appsettings はユーザー」を知る。CLI の記述はTest/Setup/AuditLogSpecDocTestが SetupCli のオプション名と突き合わせる。Standard の許可リストに verb あり - 未対応: MultiTenant ホストの Controller
- テスト:
Test/AuditLog/(Logger の記録規則/Strict/BestEffort、File/Database (SQLite) Sink、TableGuard、TestServer でミドルウェア 10 本、bulk_submit の対象と失敗)、外部ログインのコールバックはTest/Auth/ExternalLoginFlowTestの Audit_*。MVC はテストクラス内にネストしたコントローラを見つけないのでトップレベルに置く。セットアップはTest/Setup/AuditLogSetupServiceTest
- 状態: 実装済み (docs/EnterFocusMoveField.md)
- 機能: モジュール内で Enter キー押下時に次の入力要素にフォーカスを移動 (末尾→先頭ループ、tabindex尊重、IME対応)
- UI: なし (デザインモードでのみ
EnterFocusMoveラベルを表示) - JS interop:
wwwroot/enterfocusmove-interop.jsでモジュールルート ([data-module]/[data-module-design]) に keydown をバインド - 除外:
<textarea>,contenteditable,data-consumes-enter属性を持つ要素, Submit ボタン - ファイル:
Designs/EnterFocusMoveFieldDesign.cs,Fields/EnterFocusMoveField.cs,Components/EnterFocusMoveFieldComponent.razor,wwwroot/enterfocusmove-interop.js
- 状態: 実装済み (Extras.Designer FieldDocs/LoginAccountContractFieldDesign.md)
- 機能: ユーザーモジュール (CurrentUserModule) に置き、行がログインアカウントとしてどう振る舞うかを宣言する契約フィールド。役割 (フィールド参照): LoginName (必須) / ExternalLoginName (外部 IdP の突き合わせ。空なら LoginName) / IsActive (偽なら拒否) / DisplayName (Cookie の Name と TOTP の QR) / TwoFactorEmail (メール二要素の送信先)。ログイン用の書き込み専用列: DbColumnPasswordHash + Salt (照合用。空 = パスワードログイン無し)、DbColumnTotp* 3 列 (認証アプリの二要素。空 = 無効)。テンプレートのログイン (Extras.Server
PasswordLogin= 検証の流れ 1 回呼び: 契約の確認 →LoginLockout→LoginAccountStore.VerifyPasswordAsync→TotpLogin/EmailOtpLogin→ 結果PasswordLoginResult{Status(NotAvailable/Locked/Unauthorized/TwoFactor/Ok), Account(Ok だけ), Response(クライアントへそのまま), Detail, IsDenied}。HTTP・監査・Cookie はホストの AccountController。★2026-10-30 ホストの Login 80 行をここへ) はこの契約だけを見る。PasswordHashField は「書く側」として別に置く - セッションの印 (ログイン済み Cookie の失効。SecurityStamp と同じ考え): 書き込み専用列
DbColumnSessionStamp([DbColumn(SessionStamp, IsWriteOnly = true)]= DDL 生成に載る。TEXT。空 = 照合しない = 従来どおり。デザインチェックは列の存在だけ。HasSessionStamp)。値はServices/SessionStamp.Create()(サーバーだけが作る)。ログインでLoginAccountStoreが行の印を読み (NULL なら採番 = 移行スクリプト不要)、LoginPrincipal.Create(..., sessionStamp)がsession_stampクレームに積む。リクエストごとにLoginSessionValidation.ValidatePrincipalAsync(ホストが Cookie の OnValidatePrincipal から呼ぶ) がIsSessionValidAsyncで主キー 1 行を読んで照合 (キャッシュ無し = 遅延ゼロ・複数インスタンスでも DB が正本)。行無し・停止・不一致・印の無い Cookie は RejectPrincipal + SignOut (401・Cookie 削除。列を足した時点で全員再ログイン)、DB 障害は例外のまま 500 (Cookie は消さない) - 印の書き換え (= 全端末の Cookie 失効): パスワード変更 (契約の PasswordField →
ApplyPasswordHashが契約のデータに新しい印も入れる・戻り値 bool / 同じ表の別モジュールの PasswordHashField →PasswordHashingModuleDataIOがRevokeSessionsAsync)、ユーザーモジュールの保存で IsActive 偽 (同上。同じトランザクション)、管理者による全端末ログアウト (SessionRevokeButtonField が印を NULL に。次のログインで採番)。表示名だけの保存では変えない。保証は「書き換えた保存のコミット後の次の認証チェックから拒否」(全端末一括・処理中のリクエストは止めない・新しいログインは止めない)。外部 IdP はExternalLoginUser.SessionStamp、モバイルチケットも本人確認時の印を持つ。直接 SQL でパスワード / 有効フラグを変えるときは印も新しい乱数に (契約) - ファイル:
Designs/LoginAccountContractFieldDesign.cs(ContractFieldDesignBase派生。UI 無し)、Data/LoginAccountContractFieldData.cs(PasswordHash / PasswordSalt / SessionStamp)、Services/SessionStamp.cs、Extras.ServerAuth/LoginSessionValidation.cs
- 状態: 実装済み (docs/TwoFactorLogin.md)。Extras.Server
Auth/EmailOtpLogin.cs(フィールドは無し) - 機能: LoginAccountContractField の TwoFactorEmail (送信先フィールド) を設定するとパスワード成功後に 6 桁コードをメールで送り入力を求める。コードはサーバーのキャッシュ (IDistributedCache) に有効期限付き。試行超過で破棄。契約の TOTP 列があれば認証アプリ優先
- 設定: appsettings
EmailOtpLogin(MailInfraName / Subject / Body / CodeLifetimeMinutes / MaxAttempts、すべて任意)
- 状態: 実装済み (Extras.Designer FieldDocs/TotpResetButtonFieldDesign.md)
- 機能: ログインユーザーモジュールの詳細画面に置き、表示中の行のユーザーの認証アプリ (TOTP) 登録を解除する。TOTP の 3 列を
DbColumn(IsWriteOnly)で持ち、解除は「3 列を空にするデータ」を通常の Submit で書く (SubmitButtonField と同じく Module.SubmitAsync)。権限は CLB の権限モデル (UserWrite / DataWrite 条件) がそのまま効くのでサーバーに専用の入口は無い。列は契約と同じでなければならない (デザインチェック)。登録済みかは表示しない (書き込み専用列は SELECT されない) - ファイル:
Designs/TotpResetButtonFieldDesign.cs,Data/TotpResetButtonFieldData.cs,Fields/TotpResetButtonField.cs,Components/TotpResetButtonFieldComponent.razor
- 状態: 実装済み (Extras.Designer FieldDocs/SessionRevokeButtonFieldDesign.md)
- 機能: ログインユーザーモジュールの詳細画面に置き、表示中の行のユーザーを全端末からログアウトさせる。セッションの印の列を
DbColumn(IsWriteOnly)で持ち、「印を NULL にするデータ」を通常の Submit で書く (TotpResetButtonField と同じ形。Module.SubmitAsync)。印が NULL の行は照合で落ち、次のログインで採番される。権限は CLB の権限モデル (UserWrite / DataWrite 条件) がそのまま効くのでサーバーに専用の入口は無い。列は契約の DbColumnSessionStamp と同じでなければならない (デザインチェック) - ファイル:
Designs/SessionRevokeButtonFieldDesign.cs,Data/SessionRevokeButtonFieldData.cs,Fields/SessionRevokeButtonField.cs,Components/SessionRevokeButtonFieldComponent.razor
- 状態: 実装済み (Extras.Designer FieldDocs/MyTotpResetButtonFieldDesign.md)
- 機能: ログイン中の自分の認証アプリ (TOTP) 登録を解除するボタン。表示中の行とは無関係で、どのモジュールにも置ける (設定画面・マイページ)。状態はサーバー問い合わせ (列は書き込み専用)
- 結線: 共通の API の
TotpApi(TotpLoginSettings)(Totp.Status / Reset。ログイン中のユーザーが対象)。専用ルートはTotpResetClient.StatusEndPoint/ResetEndPoint(互換) - ファイル:
Designs/MyTotpResetButtonFieldDesign.cs,Fields/MyTotpResetButtonField.cs,Fields/TotpResetClient.cs,Components/MyTotpResetButtonFieldComponent.razor
- 契約の
PasswordField(同モジュールの PasswordField 参照) を指定すると、PasswordHashHelper.ApplyPasswordHashが契約のデータ (LoginAccountContractFieldData= PasswordHash / PasswordSalt。印の列があれば新しい SessionStamp も) を差し込み、本体が契約の書き込み専用列へ書く。ユーザーモジュールに PasswordHashField は不要 (併用はデザインチェック エラー・ヘルパーも契約を優先) - TOTP の 3 列は契約の列名プロパティだけ (DbColumn 属性なし)。本体の書き込みは「送られたフィールドの DbColumn メンバーを全部書く」ので、同じデータに載せるとパスワード保存で NULL 上書きされるため。
ContractFieldDesignBase.GetRolePropertiesは CandidateType.DbColumn のプロパティを役割から除外する
- 状態: 実装済み (docs/PasswordHashField.md)
- 機能: 平文
PasswordFieldを Submit 時にハッシュ+ソルトへ変換し、2つのDBカラムへ書き込む書き込み専用フィールド。UIなし - 要件: サーバ側で
CustomizedModuleDataIOがPasswordHashingModuleDataIO(Extras.ServerAuth/。本体の行書き込みAddAsync/UpdateAsync/BulkAddAsyncを override してPasswordHashHelper.ApplyPasswordHash(...)を呼ぶModuleDataIO派生) を継承するのが必須 (テンプレートは継承済み。Helper は Extras 本体に同梱)。行を書く直前の変換なので、保存だけでなくシステム権限の内部書き込み (ModuleDataIOSystemAccess・ホストのAddSystemRecordAsync) も通り、本体のフィールド書き込み権限の検査の後に入る (ハッシュ列の書き込み権限は見ない。ユーザーが書くのは PasswordField)。自前で override して ApplyPasswordHash を呼ぶ形も動く - ファイル:
Designs/PasswordHashFieldDesign.cs,Fields/PasswordHashField.cs,Data/PasswordHashFieldData.cs,Components/PasswordHashFieldComponent.razor
- 状態: 実装済み (docs/OrientationLockField.md)
- 機能: タッチ端末で画面の向き(横/縦)が指定と異なるとき、全画面オーバーレイで回転を促す。CSSメディアクエリ (
pointer: coarse) のみで制御 (JS不要) - UI: デザインモードでは
OrientationLock (...)プレースホルダを表示 - ファイル:
Designs/OrientationLockFieldDesign.cs,Fields/OrientationLockField.cs,Components/OrientationLockFieldComponent.razor
- タスクガントチャート (
TaskGantt.razor): SVGベース、ドラッグ移動/リサイズ、タスク依存線、スナップ。→GanttFieldとして製品化済み - 設備稼働ガントチャート (
DeviceStateGantt.razor): SVGベース、ステータス色分け(正常/警告/異常)、時間軸
Extrasでは以下の3ファイル構成が基本 (データなしフィールドの場合):
| ファイル | 配置先 | 役割 |
|---|---|---|
XxxFieldDesign.cs |
Designs/ |
デザイン時定義 (JSON永続化) |
XxxField.cs |
Fields/ |
ランタイムロジック |
XxxFieldComponent.razor |
Components/ |
Blazor UI + .razor.css |
値を持つフィールドは Data/XxxFieldData.cs も必要 (4ファイル構成。データクラスは Data/ に配置)。
Codeer.LowCode.Blazor.Extras.Designs # デザインクラス
Codeer.LowCode.Blazor.Extras.Fields # ランタイムクラス
Codeer.LowCode.Blazor.Extras.Components # Blazorコンポーネント
using Codeer.LowCode.Blazor.DesignLogic.Check;
using Codeer.LowCode.Blazor.DesignLogic.Refactor;
using Codeer.LowCode.Blazor.Extras.Components;
using Codeer.LowCode.Blazor.Extras.Fields;
using Codeer.LowCode.Blazor.OperatingModel;
using Codeer.LowCode.Blazor.Repository.Data;
using Codeer.LowCode.Blazor.Repository.Design;
namespace Codeer.LowCode.Blazor.Extras.Designs
{
public class XxxFieldDesign() : FieldDesignBase(typeof(XxxFieldDesign).FullName!)
{
// デザイナーで設定可能なプロパティ
[Designer(CandidateType = CandidateType.Field)]
public string SomeField { get; set; } = string.Empty;
[Designer(CandidateType = CandidateType.ScriptEvent)]
public string OnSomeEvent { get; set; } = string.Empty;
public override string GetWebComponentTypeFullName() => typeof(XxxFieldComponent).FullName!;
public override string GetSearchWebComponentTypeFullName() => string.Empty;
public override string GetSearchControlTypeFullName() => string.Empty;
public override FieldDataBase? CreateData() => null;
public override FieldBase CreateField() => new XxxField(this);
public override List<DesignCheckInfo> CheckDesign(DesignCheckContext context)
{
var result = base.CheckDesign(context);
// フィールド参照の存在チェック
context.CheckFieldFieldExistence(Name, nameof(SomeField), SomeField).AddTo(result);
return result;
}
public override RenameResult ChangeName(RenameContext context)
=> context.Builder(base.ChangeName(context))
.AddField(SomeField, x => SomeField = x)
.Build();
}
}using Codeer.LowCode.Blazor.Extras.Designs;
using Codeer.LowCode.Blazor.OperatingModel;
using Codeer.LowCode.Blazor.Repository.Data;
using Codeer.LowCode.Blazor.Script;
namespace Codeer.LowCode.Blazor.Extras.Fields
{
public class XxxField(XxxFieldDesign design) : FieldBase<XxxFieldDesign>(design)
{
public override bool IsModified => false;
public override FieldDataBase? GetData() => null;
public override FieldSubmitData GetSubmitData() => new();
public override Task SetDataAsync(FieldDataBase? fieldDataBase) => Task.CompletedTask;
[ScriptHide]
public override async Task InitializeDataAsync(FieldDataBase? fieldDataBase)
{
// 初期化ロジック
}
}
}@using Codeer.LowCode.Blazor.Components
@using Codeer.LowCode.Blazor.Extras.Fields
@inherits FieldComponentBase<XxxField>
<div>
<!-- UI実装 -->
</div>
@code {
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
}
}FieldDesignBase # 全フィールドの基底 (データなし)
├── ValueFieldDesignBase # 値を持つフィールド
│ └── DbValueFieldDesignBase # DB列にマッピングされるフィールド
└── ListFieldDesignBase # リスト系フィールド
FieldBase : IUIContext # 全フィールドの基底
└── FieldBase<TDesign> # デザイン型付き
└── ValueFieldBase<TDesign, TData, TValue> # 値あり (Source/Current/IsModified)
ISearchResultsViewFieldDesign- 検索条件を持つフィールドデザイン (SearchCondition)ISearchResultsViewField- 検索結果を表示するフィールド (SetAdditionalConditionAsync,ReloadAsync)IDataDependentField- 他フィールドのデータに依存するフィールド (GetDependencyFields())IDisplayName- 表示名を持つフィールド
TextField, NumberField, BooleanField, DateField, DateTimeField, TimeField, SelectField, RadioGroupField, LinkField, IdField, FileField, ListField, DetailListField, TileListField, SearchField, ButtonField, SubmitButtonField, LabelField, ImageViewerField, ModuleField, ProCodeField, ExecuteSqlField, QueryField
ModuleData → Module.InitializeDataAsync() → Field.InitializeDataAsync()
→ ユーザー操作
→ GetSubmitData() → ModuleDataService.SaveAsync()
// 検索してモジュール一覧取得
var items = await this.GetChildModulesAsync(searchCondition, ModuleLayoutType.Detail, layoutName);
// 新規モジュール作成
var mod = await this.CreateChildModuleAsync(moduleName, ModuleLayoutType.Detail, layoutName);
// ダイアログ表示
var result = await mod.ShowDialogAsync("OK", "Cancel");
// 保存
await mod.SubmitAsync();[Designer] // 基本プロパティ (デザイナーUIに表示)
[Designer(Scope = DesignerScope.All)] // 全スコープで表示
[Designer(CandidateType = CandidateType.Field)] // フィールド名の候補リスト
[Designer(CandidateType = CandidateType.ScriptEvent)] // スクリプトイベント
[Designer(CandidateType = CandidateType.DetailLayout)] // DetailLayout候補
[Designer(CandidateType = CandidateType.Resource)] // リソースパス候補
[ModuleMember(Member = "...")] // どのモジュールのメンバーか
[TargetFieldType(Types = [typeof(TextFieldDesign)])] // 対象フィールド型を制限
[Layout(ModuleNameMember = "...")] // レイアウト候補のモジュール指定
[ScriptMethod(ArgumentTypes = ["string"], ArgumentNames = ["id"])] // スクリプトイベントの引数定義[ScriptHide] // スクリプトから非表示
[ScriptName("Reload")] // スクリプト内での名前 (メソッド名からAsyncを除く等)
[ScriptMethodToProperty("Value")] // 非同期メソッドをプロパティとして公開- privateフィールド:
_camelCase - publicプロパティ:
PascalCase - 非同期メソッド:
Asyncサフィックス - C#: インデント4スペース / Razor, CSS: インデント2スペース
- 改行: CRLF、末尾改行あり
- コードビハインドファイルは使わない (
.razor内の@code {}に記述) - CSSスコープファイル (
.razor.css) を各コンポーネントに配置
フィールド実装の参考にする場合、本体ソースは以下から読める:
C:\tfs\codeer\Codeer.LowCode.Blazor\Source\Codeer.LowCode.Blazor\
├── Repository\Design\ # デザインクラスの実装例
├── OperatingModel\ # ランタイムクラスの実装例
├── Components\Fields\ # Blazorコンポーネントの実装例
├── Repository\Data\ # データクラスの実装例
└── DataIO\ # FieldBase拡張メソッド (GetChildModulesAsync等)
特に参考になるファイル:
ListField系:ListFieldDesignBase.cs,ListField.cs,ListFieldComponent.razorCalendarFieldの元になったパターン:ISearchResultsViewFieldの実装
(ガントチャート / カンバンボード / リッチテキストエディタ は実装済み。「既存フィールド」参照)
- チャート/ダッシュボード (ChartField) - 各種グラフ表示
- 署名パッド (SignatureField) - 手書き署名キャプチャ
- 地図 (MapField) - 位置データ可視化