本資料では、gkillの主要なプログラム構成要素と処理フローについて説明します。バックエンド(Go)とフロントエンド(Vue 3 + TypeScript)の両面をカバーします。
gkillには2つのエントリーポイントがあります。
| バイナリ | ソース | 特徴 |
|---|---|---|
gkill_server |
gkill/main/gkill_server/main.go |
ヘッドレスHTTPサーバー |
gkill |
gkill/main/gkill/main.go |
デスクトップアプリ(go-astilectron) |
両者ともgkill/main/common/パッケージの共通初期化ロジックを使用します。
sequenceDiagram
participant Main as main()
participant Common as common パッケージ
participant Options as gkill_options
participant Log as gkill_log
participant API as GkillServerAPI
participant DAO as GkillDAOManager
Main->>Common: init()
Note over Common: HOME環境変数の設定<br/>タイムゾーン修正(Android対応)
Main->>Common: InitGkillOptions()
Common->>Options: 各ディレクトリパス設定
Note over Options: LibDir, CacheDir, LogDir,<br/>ConfigDir, TLSファイルパス,<br/>DataDirectoryDefault, SkillsDir
Main->>Log: gkill_log.Init()
Note over Log: slogハンドラ設定<br/>レベル別ファイル出力
Main->>Common: InitGkillServerAPI()
Common->>API: api.NewGkillServerAPI()
API->>DAO: dao.NewGkillDAOManager()
Note over DAO: ConfigDAOs初期化<br/>(8つの設定DAO)<br/>SkillStore生成<br/>(SkillsDir 配下)
Main->>Common: LaunchGkillServerAPI()
Common->>API: Serve(ctx)
Note over API: HTTPサーバー起動<br/>再起動ループ対応
gkill_optionsパッケージで定義されるディレクトリ構造を$GKILL_HOME配下に初期化します。
func InitGkillOptions() {
os.Setenv("GKILL_HOME", filepath.Clean(os.ExpandEnv(gkill_options.GkillHomeDir)))
gkill_options.LibDir = "$GKILL_HOME/lib/base_directory"
gkill_options.CacheDir = "$GKILL_HOME/caches"
gkill_options.LogDir = "$GKILL_HOME/logs"
gkill_options.ConfigDir = "$GKILL_HOME/configs"
gkill_options.TLSCertFileDefault = "$GKILL_HOME/tls/cert.cer"
gkill_options.TLSKeyFileDefault = "$GKILL_HOME/tls/key.pem"
gkill_options.DataDirectoryDefault = "$GKILL_HOME/datas"
gkill_options.SkillsDir = "$GKILL_HOME/skills" // 利用者ごとのスキル置き場(ADR-0634)
}SkillsDir は GkillDAOManager.SkillStore(dao/gkill_dao_manager.go。skills.NewStore に展開済みパスを渡す)が
読み書きする唯一のディレクトリで、配下は <user_id>/<name>/ に分かれます。ファイルを触るのは gkill_server だけで、
MCP も設定画面も HTTP API 経由で使います(ADR-0634)。
LaunchGkillServerAPIはサーバー設定変更時の再起動をサポートしています。
func LaunchGkillServerAPI(ctx context.Context) error {
for {
err = gkillServerAPI.Serve(ctx)
if errors.Is(err, http.ErrServerClosed) {
if ctx.Err() != nil {
return nil // SIGINT/SIGTERM → 終了
}
// HandleUpdateServerConfigs → リスタート(ループ継続)
}
err = InitGkillServerAPI() // 新しいインスタンスで再初期化
}
}type GkillDAOManager struct {
stateMutex sync.Mutex // 下の4マップ自体の読み書きを保護(構築中は保持しない)
initializingMutex map[string]map[string]*sync.RWMutex // ユーザー×デバイス別初期化ロック
gkillRepositories map[string]map[string]*reps.GkillRepositories // ユーザー×デバイス別リポジトリ
gkillNotificators map[string]map[string]*GkillNotificator // 通知マネージャ
fileRepWatchCacheUpdater rep_cache_updater.FileRepCacheUpdater // ファイル監視キャッシュ更新
pluginManagers map[string]*PluginManager // ユーザーID別 PluginManager(GetRepositories 時に遅延初期化)
ConfigDAOs *ConfigDAOs // 設定データベース群
SkillStore *skills.Store // 利用者ごとのスキル($GKILL_HOME/skills/<user_id>/<name>/)のストア
router *mux.Router // HTTPルーター
IDFIgnore []string // IDF無視パターン
skipUpdateCache *atomic.Int64 // キャッシュ更新を止めている数(参照カウンタ)
}SkillStore は NewGkillDAOManager が ConfigDAOs と同時に生成します(skills.NewStore(filepath.Clean(os.ExpandEnv(gkill_options.SkillsDir)))、
dao/skills/ パッケージ)。設定 DAO 群とは違って SQLite ではなくディレクトリ直下のファイルを扱い、
スキル系ハンドラ6本(get_skill_list / get_skill / download_skill / upload_skill / write_skill_file / delete_skill)が
g.GkillDAOManager.SkillStore を通して読み書きします。
graph TD
subgraph ConfigDAOs
SC[ServerConfigDAO<br/>server_config.db]
AC[AccountDAO<br/>account.db]
LS[LoginSessionDAO<br/>account_state.db]
FU[FileUploadHistoryDAO<br/>account_state.db]
SK[ShareKyouInfoDAO<br/>share_kyou_info.db]
AP[ApplicationConfigDAO<br/>user_config.db]
RP[RepositoryDAO<br/>user_config.db]
NT[GkillNotificationTargetDAO<br/>gkill_notification_target.db]
end
subgraph "SQLite3ファイル"
F1[(server_config.db)]
F2[(account.db)]
F3[(account_state.db)]
F4[(share_kyou_info.db)]
F5[(user_config.db)]
F6[(gkill_notification_target.db)]
end
SC --> F1
AC --> F2
LS --> F3
FU --> F3
SK --> F4
AP --> F5
RP --> F5
NT --> F6
| DAO | DBファイル | 管理対象 |
|---|---|---|
| ServerConfigDAO | server_config.db | サーバーアドレス、TLS設定、デバイス名 |
| AccountDAO | account.db | ユーザーアカウント、パスワードハッシュ |
| LoginSessionDAO | account_state.db | ログインセッション(30日有効) |
| FileUploadHistoryDAO | account_state.db | ファイルアップロード履歴 |
| ShareKyouInfoDAO | share_kyou_info.db | 記録の共有設定 |
| ApplicationConfigDAO | user_config.db | アプリケーション設定 |
| RepositoryDAO | user_config.db | リポジトリ定義(RepType、パス等) |
| GkillNotificationTargetDAO | gkill_notification_target.db | プッシュ通知ターゲット |
ユーザー×デバイスの組み合わせごとに生成されるデータリポジトリの集合です。
graph LR
subgraph GkillRepositories
K[KmemoReps]
KC[KCReps]
U[URLogReps]
T[TimeIsReps]
M[MiReps]
N[NlogReps]
L[LantanaReps]
TG[TagReps]
TX[TextReps]
NF[NotificationReps]
RK[ReKyouReps]
MRK[MiReKyouReps]
ID[IDFKyouReps]
GP[GPSLogReps]
GC[GitCommitLogReps]
PL[PluginReps]
end
subgraph "書き込み先(各1つ)"
WK[WriteKmemoRep]
WKC[WriteKCRep]
WU[WriteURLogRep]
WT[WriteTimeIsRep]
WM[WriteMiRep]
WN[WriteNlogRep]
WL[WriteLantanaRep]
WTG[WriteTagRep]
WTX[WriteTextRep]
WNF[WriteNotificationRep]
WRK[WriteReKyouRep]
WMRK[WriteMiReKyouRep]
WID[WriteIDFKyouRep]
WGP[WriteGPSLogRep]
end
各RepTypeに対して、読み取り用は複数のリポジトリを保持可能ですが、書き込み先(Write*Rep)は1つだけです。同じRepTypeで複数のUseToWrite=trueがあるとエラーになります。
PluginReps だけは他と性質が異なります。RepType の switch 文では生成されず、
PluginManager.DiscoverPlugins() が $GKILL_HOME/plugins/{userID}/ を走査して登録し、
PluginReps に追加されます(gkill_dao_manager.go の DiscoverPlugins 後の登録ループ)。書き込み先はありません。
Reps(Kyou 検索の対象)へ入るかは manifest.json の emits_kyou 次第です。
emits_kyou: false(位置情報履歴プラグインのような GPSLog 専用のもの)は PluginReps(設定・死活確認)と
GPSLogReps にだけ入り、Reps には入りません。Kyou を1件も返さないプラグインを
記録保管場所の一覧に出さないためです。
gkill/api/gkill_server_api/パッケージ(handle_*.go 121ファイル、1ハンドラ1ファイル)がAPIの中心です。旧gkill/api/gkill_server_api.go(約3,500行)から分割・移動されました。
- HTTPサーバーの起動・停止(
serve.go,close.go) - 全98エンドポイント(97 POST + 1 GET)のハンドリング(
handle_*.go)。GETはurlog_bookmarklet_pageのみ。ルート表(gkill_server_api_address.goのapiRoutes())が正本で、serve.goとテストハーネスがそのまま登録する - GkillDAOManagerの保持・提供
- 認証ミドルウェアによるセッション検証(
auth_middleware.go) - レスポンス構築
- アクセスログミドルウェア(gorilla/mux
Use()で全ルートに適用。リモートIP・メソッド・パス・ステータス・所要時間・ユーザIDをACCESSレベルで記録。実装:gkill_server_api_access_log.go) - gzip圧縮ミドルウェア(
gzip_middleware.go。recover・アクセスログの次にUse()で登録。/api/前方一致かつリクエストのAccept-Encodingに gzip があるときだけ圧縮する。Range配信する/files//zip_cache/(既に圧縮済みのメディアが主)と静的アセットは対象外。Content-Lengthは付けず、Flush()は gzip バッファを掃いてから下位の Flusher へ委譲するのでストリーミング応答も壊れない)
ハンドラ登録時に3つのラッパー関数を使い分けて認証レベルを指定します。
| ラッパー関数 | 件数 | 認証レベル | AuthContextの内容 | 用途 |
|---|---|---|---|---|
wrapNoAuth |
14 | ミドルウェアでの認証なし(filterLocalOnly は通る)。ボディを読む13本は wrapNoAuthCapped で経路別のボディ上限つき |
— | login, logout, reset_password, set_new_password, get_shared_kyous, urlog_bookmarklet 等 |
wrapAuth |
25 | セッション認証 | Account, UserID, Device | get_application_config, update_server_configs, add_user, generate_tls_file, update_cache, プラグイン4本 等 |
wrapAuthRepos |
59 | セッション+リポジトリ | Account, UserID, Device, Repositories | データCRUD系ハンドラ |
logout/reset_password/set_new_passwordはwrapAuthではなくwrapNoAuthです (ルート表ではAuth: authNone)。セッションの検証はハンドラ内で行います。また
wrapNoAuthは「認証を一切しない」という意味ではありません。ルータ上は未認証ですが、upload_files/upload_gpslog_files/upload_skill/browse_zip_contents/get_idf_kyou_by_relative_path/get_kyous_mcp/get_rep_infos_mcpの7本は ハンドラ内部のgetAccountFromSessionIDでセッションを検証します (MCP 向けの2本はresolveSelfAuthContext経由。中で同じ関数を呼びます)。/files/と/zip_cache/はボディではなく cookie(gkill_session_id)で認証します。
auth_context.goで定義。認証ミドルウェアが抽出した情報を格納し、各ハンドラに渡します。
type AuthContext struct {
Account *account.Account
UserID string
Device string
Repositories *reps.GkillRepositories // Auth-onlyルートではnil
}Device は *reps.Device ではなく端末名の文字列です。
authMiddleware: リクエストからsession_idを取得 → LoginSessionDAOでセッション検証 → AccountDAO からアカウント取得 →g.GetDevice()(ServerConfig 参照)で端末名を取得 →AuthContext.Account/UserID/Deviceに設定authWithReposMiddleware: 上記に加え → GkillDAOManager.GetRepositories() →AuthContext.Repositoriesに設定
DeviceDAO というDAOは存在せず、両ミドルウェアとも g.GetDevice() を使います。
Device は auth-only ルートでも設定され、wrapAuthRepos との差は Repositories の有無だけです。
GetRepositories() は、書き込み先ではない記録保管場所の組み立てに失敗した場合、その1本だけを切り離し、
利用可能な場所で処理を続ける。設定の IsEnable は書き換えない。切り離した場所は Web 検索では
MSG000090、MCP検索では warnings で通知し、利用可能な場所だけを一覧・検索対象へ載せる。
個別の切り離しと構築完了時の集約はともに Error レベルで記録する。正常時には集約エラーを出さない。
書き込み先の組み立て失敗や、切り離せない全体障害は従来どおり認証処理を失敗させ、
ERR000018、HTTP 500、Error ログの3経路へ同じ障害を伝播する。
gkill/usecase/パッケージ(19ファイル)は、ハンドラから抽出されたHTTP非依存のビジネスロジックを提供します。
- DAO/リポジトリ型を直接操作する関数群
- HTTPリクエスト/レスポンスに依存しない
- ハンドラとMCPサーバーの両方から再利用可能
| カテゴリ | エンドポイント数 | 内訳 |
|---|---|---|
| 認証 | 6 | login, logout, reset_password, set_new_password, add_user, update_account_status |
| データ取得 | 25 | get_kyous, get_kyou, get_kmemo, get_kc, get_urlog, get_nlog, get_timeis, get_mi, get_lantana, get_rekyou, get_mirekyou, get_rekyous_by_target_id, get_mirekyous_by_target_id, get_git_commit_log, get_idf_kyou, get_mi_board_list, get_all_tag_names, get_all_rep_names, get_tags_by_id, get_tag_histories_by_tag_id, get_texts_by_id, get_text_histories_by_text_id, get_gkill_notifications_by_id, get_gkill_notification_histories_by_notification_id, get_idf_kyou_by_relative_path |
| データ追加 | 12 | add_kmemo, add_kc, add_urlog, add_nlog, add_timeis, add_mi, add_lantana, add_tag, add_text, add_gkill_notification, add_rekyou, add_mirekyou |
| データ更新 | 13 | update_kmemo, update_kc, update_urlog, update_nlog, update_timeis, update_mi, update_lantana, update_tag, update_text, update_gkill_notification, update_rekyou, update_mirekyou, update_idf_kyou |
| 共有 | 5 | get_share_kyou_list_infos, add_share_kyou_list_info, update_share_kyou_list_info, delete_share_kyou_list_infos, get_shared_kyous |
| 通知 | 2 | get_gkill_notification_public_key, register_gkill_notification |
| 設定 | 7 | get_application_config, update_application_config, get_server_configs, update_server_configs, get_repositories, update_user_reps, reload_repositories |
| KFTL | 2 | submit_kftl_text, parse_kftl_text |
| トランザクション | 2 | commit_tx, discard_tx |
| キャッシュ | 1 | update_cache |
| ファイル | 4 | upload_files, upload_gpslog_files, get_gps_log, browse_zip_contents |
| プラグイン | 4 | get_plugin_list, get_plugin_content_html, get_plugin_config_html, post_plugin_config |
| スキル | 6 | get_skill_list, get_skill, download_skill, upload_skill, write_skill_file, delete_skill |
| その他 | 8 | generate_tls_file, open_directory, open_file, urlog_bookmarklet, urlog_bookmarklet_page(唯一の GET), get_updated_datas_by_time, get_kyous_mcp, get_rep_infos_mcp |
gkill/api/gkill_server_api/gkill_server_api_address.go の apiRoutes() が返すルート表で定義されます(98件。パス・HTTPメソッド・認証区分・無認証ボディ上限・ハンドラを1行1ルート)。大半はPOST /api/{endpoint}形式ですが、urlog_bookmarklet_page のみ GET です。serve.go の registerAPIRoutes が表の認証区分(authNone / authSession / authSessionRepos)から wrapNoAuth(capped 含む)/ wrapAuth / wrapAuthRepos を選んで登録し、テストハーネス(gkill_server_api_test.go の setupTestRouter)も同じ関数を呼びます。表・ハンドラ・doc コメント・認証区分の整合は api_routes_test.go が、Web クライアント(gkill-api.ts)との整合は gkill-api.test.ts が機械検査します(ADR-0709)。
API 以外のルートは19件(PathPrefix 18 + Path 1)で、SPA 配信・/files/・/zip_cache/・
/resources/manual/ 等がここに含まれます。
すべてのAPIレスポンスは以下の共通構造を持ちます。
type Response struct {
Messages []GkillMessage `json:"messages"`
Errors []GkillError `json:"errors"`
// + エンドポイント固有のフィールド
}
type GkillError struct {
ErrorCode string `json:"error_code"`
ErrorMessage string `json:"error_message"`
}- HTTP 200: 正常応答(
errorsは[]。2026-09-15 まではnullだったので、クライアントの?? []ガードは残す) - HTTP 400/401/403/404/409/413/429/500: 失敗。
error_codeから決まる (正本はsrc/server/gkill/api/message/http_status.go、経緯は ADR-0706) - ボディの形はステータスによらず同じで、
error_codeは本文にしか入っていない
関連資料: クラス階層の詳細は class-diagrams.md(セクション2)、RepTypeごとの実装選択ロジックは dvnf-rep-type-spec.md(セクション5)を参照。
各データ型のリポジトリは、以下の4層で実装されています。
graph TD
subgraph "層1: インターフェース"
IF["*Repository<br/>(例: KmemoRepository)"]
end
subgraph "層2: SQLite3実装"
S3["*RepositorySQLite3Impl<br/>直接DB操作"]
end
subgraph "層3: キャッシュ付き実装"
CA["*RepositoryCachedSQLite3Impl<br/>複数リポジトリをインメモリ統合"]
end
subgraph "層4: テンポラリ実装"
TM["*RepositoryTempSQLite3Impl<br/>一時的なトランザクション用"]
end
IF --> S3
IF --> CA
IF --> TM
CA --> S3
加えて、ローカルストレージキャッシュ版(*SQLite3ImplLocalCached)があります。
graph TD
A{UseToWrite?} -->|Yes| B[SQLite3Impl<br/>通常実装]
A -->|No| C{CacheRepsLocalStorage?}
C -->|Yes| D[SQLite3ImplLocalCached<br/>ローカルキャッシュ]
C -->|No| B
B --> E{CacheInMemory?}
D --> E
E -->|Yes| F[CachedSQLite3Impl<br/>でラップ]
E -->|No| G[そのまま使用]
KFTL(gkill独自のテキスト形式)は、テキストベースで複数種類の記録を一括入力するための書式です。
gkill/api/kftl/kftl_factory.goが中心です。
type kftlFactory struct {
// KFTLテキストからリクエストを生成するファクトリ
}- KFTLテキストを受け取る
- テキストをステートメントに分割(スプリッタ定数で区切り)
- 各ステートメントを型判定
- 型に応じたリクエスト(Add/Update)を生成
- リクエストを実行
KFTLは以下のステートメント型をサポートしています。
サーバ側の具象型は src/server/gkill/api/kftl/*.go の kftl*StatementLine 構造体(50個)。
1つのデータ型が複数行で構成されるため、行の役割ごとに型が分かれている。
| カテゴリ | ステートメント型(Go) | 説明 |
|---|---|---|
| メモ | kftlKmemoStatementLine |
テキストメモの追加 |
| 数値 | kftlStartKC / kftlKCTitle / kftlKCNumValue |
開始 → タイトル → 数値 |
| 気分 | kftlStartLantana / kftlLantanaMood |
開始 → 気分値 |
| タスク | kftlStartMi / kftlMiTitle / kftlMiBoardName / kftlMiEstimateStartTime / kftlMiEstimateEndTime / kftlMiLimitTime |
開始 → タイトル → ボード名 → 見積開始 → 見積終了 → 期限 |
| 既存の記録をタスク化 | kftlStartMiReKyou / kftlMiReKyouBoardName / kftlMiReKyouEstimateStartTime / kftlMiReKyouEstimateEndTime / kftlMiReKyouLimitTime / kftlMiReKyouTag / kftlEndMiReKyou |
開始 → ボード名 → 見積開始 → 見積終了 → 期限 → タグ → 終了(タイトル行は無い。タグは板名の前にも書ける) |
| 支出 | kftlStartNlog / kftlNlogShopName / kftlNlogTitle / kftlNlogAmount / kftlNlogRelatedTime |
開始 → 店名 →(タイトル → 金額 →(タグ / テキスト))の繰り返し。支払い1組ごとに1リクエストで、金額の行のあとのタグ・テキストはその支払いに付く。関連時刻だけはブロック全体に効く(ーん より前のタグ・テキストはエラー) |
| ブックマーク | kftlStartURLog / kftlURLogURL / kftlURLogTitle |
開始 → URL → タイトル |
| 打刻 | kftlStartTimeIs / kftlTimeIsTitle / kftlTimeIsStartTime / kftlTimeIsEndTime |
開始+終了を同時指定 |
| 打刻(開始のみ) | kftlStartTimeIsStart / kftlTimeIsStartTitle |
|
| 打刻(終了) | kftlStartTimeIsEnd / kftlTimeIsEndTitle / kftlStartTimeIsEndIfExist |
存在する場合のみ終了する派生あり |
| 打刻(タグで終了) | kftlStartTimeIsEndByTag / kftlStartTimeIsEndByTagIfExist / kftlTimeIsEndByTagTag |
|
| タグ | kftlTagStatementLine |
タグ付け |
| テキスト | kftlStartText / kftlText / kftlEndText |
開始 → 本文 → 終了 |
| 関連時刻 | kftlRelatedTimeStatementLine |
関連時刻の指定 |
| 区切り | kftlSplit / kftlSplitAndNextSecond |
ステートメント区切り(、 / 、、) |
| その他 | kftlNoneStatementLine |
該当なし |
クライアント側(
src/client/classes/kftl/kftl_*/)は同じ構成で 53 クラス(Go が旗で共用する「存在時のみ終了」のタイトル行などを別クラスにしている)。 ただしクライアント側は行ラベルの分類器だけで、解釈(行別エラー・繰り返しの展開)と書き込みはサーバの1実装が担う (ADR-0507)。Web もPOST /api/submit_kftl_textで送り、 「おかしな行」はPOST /api/parse_kftl_text(KFTLStatement.Analyze。submit と同じprepareRequests)の応答で塗る。 保存マーカー「!」の行は値の行に数えず、内容の無い記録(種別だけ・店名だけの支出・空の数値)、付け先の無いタグ・関連時刻、 読めない予定日時は書く前に行別エラーにする(ADR-0508)。notification/template/time_setというステートメント型は存在しません。
サーバ側パーサ(kftl_factory.go)とクライアント側パーサ(src/client/classes/kftl/)は、日本語プレフィックスと ASCII プレフィックスの両方を常に受理する。例: タグは 。tag でも #tag でも受理される。セーブ文字は ! でも ! でも受理される。これにより非日本語ロケールのユーザも問題なく KFTL を使用できる。クライアント側の ASCII プレフィックス定数と判定・除去ヘルパーは src/client/classes/kftl/kftl-prefixes.ts に集約されている。
タグ行の複数タグ区切りは全角 、 と半角 , の両方を受理する(例: #tag1,tag2、。tag1、tag2)。タグで打刻終了(ーたえ//endt、ーいたえ//endt?)のタグ名行も同様。
Mi の時間フィールド(limitTime, estimateStartTime, estimateEndTime)と TimeIs(ーち//timeis)の開始・終了時刻行でも、全角 ? と ASCII ? の両方を関連時刻プレフィックスとして受理する。
KFTLパーサーはコンテキストから現在時刻を計算します。
nowFromCtx(ctx) = ctx.BaseTime + AddSecond * second
テンプレート内の時刻指定により、記録時刻をオフセットできます。
| キャッシュ種別 | フラグ | 説明 |
|---|---|---|
| インメモリ | --cache_in_memory (デフォルトtrue) |
全リポジトリデータをメモリ上のSQLite3に集約 |
| ローカルストレージ | --cache_reps_local |
ローカルファイルにキャッシュコピーを保持 |
| LatestDataRepositoryAddress | — | 各データのIDから最新リポジトリアドレスを引くキャッシュ |
graph LR
subgraph "複数のSQLite3ファイル"
R1[(rep1.db)]
R2[(rep2.db)]
R3[(rep3.db)]
end
subgraph "CachedSQLite3Impl"
MC[(メモリDB<br/>:memory:)]
end
R1 --> MC
R2 --> MC
R3 --> MC
MC --> Q[検索クエリ]
複数のリポジトリファイルのデータを1つのインメモリSQLite3データベースに集約し、検索パフォーマンスを向上させます。
集約後のテーブルは行ごとに実リポジトリ名(REP_NAME)を持つので、「どのリポジトリの記録か」は
キャッシュを剥がさなくても判別できます。
UnWrap()するとキャッシュ層を丸ごと飛び越える。 キャッシュrepのUnWrap()は配下の生ディスクrepを返します。返ってきたrepをそのまま検索に使うと、 インメモリキャッシュも--cache_reps_localのローカルコピー層も外れ、元のDBファイルを直接舐めることになります。 実データの一例では十数個のキャッシュrepが数百個の生rep(大半が端末別の重複登録)に展開され、 git リポジトリだけで1窓あたり十数秒(実質CPUの過半)を使っていました。
UnWrap()を使ってよいのは「そのラッパに該当する実repが1つでもあるか」を判定する枝刈りと、 rep名の一覧(GetAllRepNames)だけです。検索対象にはラッパのまま入れ、 絞り込みは結果側(Kyou.RepName)で行います。詳細は sequence-diagrams.md の「7. Kyou 検索」を参照。
| パラメータ | デフォルト | 説明 |
|---|---|---|
--cache_clear_count_limit |
3000 | キャッシュアイテム数上限 |
--cache_update_duration |
1m | キャッシュ自動更新間隔 |
FileRepCacheUpdaterが対象リポジトリファイルの変更をfsnotifyで監視し、変更を検出するとキャッシュを自動更新します。
sequenceDiagram
participant Client
participant API as GkillServerAPI
participant DAO as AccountDAO
participant Session as LoginSessionDAO
Client->>API: POST /api/login<br/>{user_id, password_sha256}
API->>DAO: パスワード照合(Argon2id)
DAO-->>API: OK
API->>Session: セッション作成(30日有効)
Session-->>API: session_id
API-->>Client: {session_id}
Note over Client,API: 以降のリクエストにはsession_idを付与
Client->>API: POST /api/get_kyous<br/>{session_id, ...}
API->>Session: セッション検証
Session-->>API: OK(有効期限内)
API-->>Client: レスポンス
| 項目 | 値 |
|---|---|
| パスワードハッシュ | Argon2id(クライアントが送るSHA256 hexに対して適用) |
| セッション有効期限 | 30日 |
| セッションのその他の失効契機 | パスワードを設定しなおすと、そのユーザの全セッションを削除する(DeleteLoginSessionsByUserID)。他端末のログインも落ちる |
| リセットトークン有効期限 | 72時間(account.PasswordResetTokenTTL)。単回使用 |
| 初期ユーザー | admin(パスワードなし) |
| セッションストレージ | account_state.db(SQLite3) |
| 技術 | バージョン | 用途 |
|---|---|---|
| Vue 3 | ^3.5.40 | UIフレームワーク |
| Vuetify 4 | ^4.1.7 | UIコンポーネントライブラリ |
| Vue Router 5 | ^5.2.0 | ルーティング |
| vue-i18n 11 | ^11.4.8 | 国際化(7言語) |
| Vite 8 | ^8.2.0 | ビルドツール |
| TypeScript 6 | ~6.0.0 | 型安全性 |
| vite-plugin-pwa | ^1.2.0 | PWA対応 |
バージョンは package.json を正とする。詳細な依存一覧は frontend-architecture.md を参照。
全13ルートの定義一覧は screen-transition.md を、各画面の項目定義は screen-specs.md を参照。
主要ルート: /(ログイン), /kftl(KFTL入力), /mi(タスクボード), /rykv(履歴閲覧), /kyou(記録詳細), /mkfl(打刻メモ帳), /playing(打刻一覧), /dashboard(日次サマリー), /rudbeckia(ポート), /saihate(記録特化), /set_new_password(パスワード変更), /register_first_account(初回登録), /shared_page(共有)
この13ルートに加えて、コンポーネントを持たないリダイレクト専用ルートが2つある:
| 旧パス | 行き先 | 備考 |
|---|---|---|
/regist_first_account |
/register_first_account |
reset_token クエリを引き継ぐ |
/shared_mi |
/shared_page |
share_id クエリを引き継ぐ |
サーバ側(serve.go)も旧パスの PathPrefix を残しており、SPA を配信して vue-router に寄せる。
旧パスの吸収はルータの
redirectで行うこと。 空のページを置いて setup からrouter.replaceしてはいけない。共有ページは<script setup>に top-level await のある 非同期コンポーネントで、初回ナビゲーションの解決中にその中から新しいナビゲーションを始めると 遷移が完了しなくなる(page.gotoが60秒待っても返らない)。
src/client/classes/api/gkill-api.ts(約3,600行)は、バックエンドAPIとの通信を一元管理するシングルトンクラスです。
- 全90登録エンドポイントへのHTTPリクエスト送信
- セッションIDの管理
- リクエスト/レスポンスの型変換
- エラーハンドリング
gkillはPiniaやVuexを使用せず、Props/Emitパターンのみで状態を管理しています。
- 親→子: Propsでデータ渡し
- 子→親: Emitでイベント通知
- API通信:
GkillAPIシングルトン経由
Kyou の削除は Kyou 単体の論理削除ではなく連鎖削除です。処理はサーバの1実装(/api/delete_kyou_cascade → usecase.DeleteKyouCascade)で、src/client/classes/cascade-delete-kyou.ts はそれを1回呼ぶだけです。MCP の gkill_delete_kyou(既定で連鎖)も同じ実装を使います(ADR-0636。以前はこのファイルが探索と tx の組み立てを持っていて、MCP は対象1件だけを消していた)。
- 探索(readのみ) — 幅優先で辿ります。1件のidにつき付随する Tag / Text / Notification と、そのidを参照する ReKyou / MiReKyou を逆引きし、見つかった ReKyou / MiReKyou をさらに辿ります。訪問済みid集合で循環参照を止め、深さ上限は32(
usecase.MaxCascadeDeleteDepth)。探索が1つでも失敗したら何も書きません。 - 削除(write) — Tag/Text/Notification → ReKyou(深い方から)→ MiReKyou → 最後に Kyou 自身の順で、全件に同じ
update_time(現在の版と同じ秒なら1秒進める)とis_deleted=trueを付けて積みます。 - 全件を1つの tx で積み、
CommitTxで確定します(ADR-0410)。1つの SQLite トランザクション(ADR-0219)なので、全部消えるか何も消えないかのどちらかです。応答のdeletedに消した全件の{id, data_type}が返り、画面はそのぶんの Service Worker キャッシュを落として、行になる種別のぶんだけ一覧から取り除きます。 - 共有画面(
application_config.for_share_kyou)では削除自体を行いません。
サーバのエラーコードは ERR000445〜ERR000451(起点が無い 448・削除済み 449・起点にできない種別 450・深さ超過 451・積み込みや確定の失敗 447)、画面側の想定外の失敗は ERR900094(cascade_delete_failed)です。文言は FAILED_DELETE_KYOU_NOTHING_DELETED_MESSAGE / CASCADE_DELETE_DEPTH_EXCEEDED_MESSAGE です。呼び出し元の use-confirm-delete-kyou-view.ts はダイアログのクローズを finally に置き、例外が出ても「消えているのに閉じない」状態にならないようにしています。
| 種別 | 数 | 配置 |
|---|---|---|
| ページ | 15 | pages/*.vue |
| ビュー | 207 | pages/views/*.vue |
| ダイアログ | 119 | pages/dialogs/*.vue |
Vuetifyで2つのテーマを定義しています。
| テーマ名 | 種類 |
|---|---|
gkill_theme |
ライトテーマ |
gkill_dark_theme |
ダークテーマ |
vite-plugin-pwaとWorkboxによるプレキャッシュ- POSTリクエストのオフラインキャッシュ
- プッシュ通知(VAPID)
- Web Share Target(他アプリからの共有受け取り)
cobraライブラリで定義されているCLIサブコマンド一覧です。
| コマンド | 説明 |
|---|---|
version |
バージョン情報表示(バージョン、ビルド日時、コミットハッシュ、ビルドした作業ツリーの tree hash。tree: 行は E2E の attestation が読む) |
idf |
指定ディレクトリのIDF(IDファイル)生成。-i/--ignore で除外ファイル名を指定。gkill_server にのみ登録されており、デスクトップアプリ gkill からは使えない |
dvnf |
DVNFファイル操作(get [dvnfPath] / copy src target / move src target)。共通フラグ --new/-n、--auto_create、--device。詳細は dvnf-rep-type-spec.md |
generate_thumb_cache |
サムネイルキャッシュ生成 |
generate_video_cache |
動画キャッシュ生成 |
optimize |
リポジトリ最適化 |
update_cache |
キャッシュ更新(稼働中サーバーにHTTPリクエスト。認証情報の指定は不要で、管理者名義の短命セッションをローカルDBへ自己発行して使う) |
clear_cache |
ディスク上の派生キャッシュ削除(<thumb|video|zip|plugin|all> <all|user_id...>。allで全体、user_id指定で該当ユーザー分のみ) |
generate_plugin_cache |
プラグインのキャッシュを稼働中サーバ無しで同期構築(<plugin_name|all> <user_id...>。各プラグインバイナリを --gkill-build-cache で単独起動して終わるまで待つ。stdout の結果行 built / no_cache 以外は失敗。main/common/generate_plugin_cache.go) |
add_tag |
検索条件 JSON(FindKyouQuery)に一致する Kyou へタグを付与(<user_id...> --rules_file <path>。稼働中サーバの HTTP クライアント。main/common/add_tag.go) |
reset_password |
指定アカウントのパスワードを無効化し、リセットトークンを再発行してURLを表示(ユーザーID...)。account.db を直接開く。管理者がパスワードを忘れたときの唯一の復帰経路 |
mcp |
MCP サーバを起動(--kind read|write|readwrite、--transport stdio|http、--config <path>。起動中の gkill_server への HTTP クライアント。main/common/mcp.go)。gkill_server にのみ登録されており、デスクトップアプリ gkill からは使えない |
mcp schema-budget |
tools/list のバイト量を予算ファイルと突き合わせる(--update で書き直す)。mcp のサブコマンド |
gkill_server / gkill の両方に定義されるフラグです(main/gkill_server/main.go:25-36、main/gkill/main.go)。
| フラグ | 既定値 | 説明 |
|---|---|---|
--gkill_home_dir |
$HOME/gkill |
データ/設定/ログのホームディレクトリ |
--address |
(なし) | リッスンアドレスを上書き(例: :19999)。設定DBの ADDRESS は書き換えない実行時オーバーライド |
--disable_tls |
false |
TLS強制を無効化 |
--cache_in_memory |
true |
リポジトリデータをメモリにキャッシュ |
--cache_reps_local |
false |
リポジトリをローカルにキャッシュ |
--goroutine_pool |
runtime.NumCPU() |
ゴルーチンプールサイズ |
--cache_clear_count_limit |
3000 |
キャッシュクリアまでのアイテム数上限 |
--cache_update_duration |
1m |
キャッシュ更新間隔 |
--pre_load_users |
(なし) | 起動時にリポジトリを先読みするユーザ(複数指定可)。PreLoadRepositories が処理する |
--log |
none |
ログレベル: none, error, warn, info, access, debug, trace, trace_sql |
--log_rotate_max_bytes |
33554432(32 MiB) |
ログファイル1本の上限。超える直前に回転する。0以下で回転を無効化 |
--log_rotate_keep |
5 |
回転した旧ファイルの保持世代数(.1〜.5)。0以下で旧ファイルを保持しない |
pluginRepositoryImpl (src/server/gkill/dao/reps/plugin_repository_impl.go) がプラグインバイナリをサブプロセスとして管理する。
| 設計判断 | 内容 |
|---|---|
| Mutex の位置 | pluginRepositoryImpl struct に sync.Mutex を保持。pluginProcess struct には置かない |
| ロック範囲 | callCommand() が mu.Lock() → ensureStarted() → sendRequest() → mu.Unlock() を直列化 |
| プロセス起動 | exec.CommandContext(context.Background(), ...) — HTTP リクエストキャンセルによる強制終了を防ぐ |
| 呼び出しタイムアウト | callCommand() は呼び出し側 ctx からキャンセルを切り離し(context.WithoutCancel)、Deadline だけを引き継ぐ。期限が無ければ既定 30 秒を注入する。超過時は Process.Kill() でサブプロセスを回収し started=false にする |
| 呼び出し元のキャンセル | HTTP クライアントの切断などでは待つのをやめるだけで、プロセスには触らない。遅れて届く応答は resp.ID の不一致で読み捨てる |
| stdout の読み取り | プロセスごとに常駐リーダー goroutine 1本。bufio.Scanner を触るのはリーダーだけ |
| クラッシュ復旧 | sendRequest() 失敗時に回収 → ensureStarted() → sendRequest() を1回リトライ。ただし 打ち切りが原因のときはリトライしない |
プロセス起動自体は context.Background() なので HTTP リクエストのキャンセルでは死にません。
個々の呼び出しも、呼び出し元のキャンセルではプロセスに手を出しません。
順番待ち(実行スロット待ち、既定10秒 maxPluginQueueWait)を打ち切った場合も同様で、
ErrPluginBusy を返すだけです。
プロセスごと落とされるのは gkill 自身のデッドライン(既定30秒 / IsAlive の5秒)を
超えたときだけです。このデッドラインは実行スロットを取ってから張ります。
先に張ると行列に並んでいるだけで期限を食い潰し、正常なプラグインを殺してしまいます。
大きな HTML レスポンス(会話コンテンツ等)に対応するため、親側は 32MB バッファを設定:
scanner.Buffer(make([]byte, 32*1024*1024), 32*1024*1024)プラグイン SDK 側(plugin/sdk/sdk.go:83-84)は 1MB のままです。リクエスト JSON は
そこまで大きくならないためで、拡張しているのは親側だけです。
詳細は plugin-system.md を参照。
- folder-structure.md — ディレクトリ構成
- dvnf-rep-type-spec.md — DVNF/RepType仕様
- class-diagrams.md — クラス階層図
- sequence-diagrams.md — シーケンス図
- api-endpoints.md — APIエンドポイント一覧
- frontend-architecture.md — フロントエンド設計ガイド
- plugin-system.md — プラグインシステム仕様