dsh-tiddlywiki 0.30.47 → 0.30.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/CHANGELOG.md +2 -0
- package/lib/index.js +8146 -8089
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index-git.ts +0 -2
- package/src/index-routes.ts +381 -0
- package/src/index.ts +53 -271
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tiddlywiki",
|
|
3
3
|
"description": "把 TiddlyWiki 5 变成你和 Agent 共用的本地文档中心(纯文本 + git):你在 DSH 界面里阅读、写作、记日记,Agent 用 15 个 tiddlywiki_* 工具读写同一批笔记;每条笔记都是你能读、能改、能审计、能随时带走的文件——不是自动记忆插件,不做后台抓取。可选一键发布到微信公众号草稿箱。 Turn TiddlyWiki 5 into a document center you and your agent share, in plain text under git — a knowledge base you can read, edit, audit and take with you, not an auto-memory plugin. Mounts via the official dsh plugin system; no DSH source changes.",
|
|
4
|
-
"version": "0.30.
|
|
4
|
+
"version": "0.30.49",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dsh",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"typecheck": "tsc --noEmit",
|
|
92
92
|
"smoke:client": "node scripts/smoke-client.mjs",
|
|
93
93
|
"selftest": "node scripts/selftest.mjs",
|
|
94
|
-
"verify:static": "node scripts/verify-send-to-agent-bundle.mjs && node scripts/verify-render-bundle.mjs && node scripts/verify-wechat-publish-bundle.mjs && node scripts/verify-package-contents.mjs && node scripts/verify-version-consistency.mjs && node scripts/verify-constants.mjs && node scripts/verify-line-endings.mjs && node scripts/verify-doc-links.mjs",
|
|
94
|
+
"verify:static": "node scripts/verify-send-to-agent-bundle.mjs && node scripts/verify-render-bundle.mjs && node scripts/verify-wechat-publish-bundle.mjs && node scripts/verify-package-contents.mjs && node scripts/verify-version-consistency.mjs && node scripts/verify-constants.mjs && node scripts/verify-index-barrel.mjs && node scripts/verify-line-endings.mjs && node scripts/verify-doc-links.mjs",
|
|
95
95
|
"verify:unit": "node scripts/verify-clip-bridge.mjs && node scripts/verify-seed-error-policy.mjs && node scripts/verify-render-sanitizer.mjs && node scripts/verify-secret-masking.mjs && node scripts/verify-write-policy.mjs && node scripts/verify-write-call-sites.mjs && node scripts/verify-workspace.mjs && node scripts/verify-wiki-registry.mjs && node scripts/verify-wiki-farm.mjs && node scripts/verify-session-scope.mjs && node scripts/verify-skill-install.mjs && node scripts/verify-repo-keying.mjs && node scripts/verify-restart-drain.mjs && node scripts/verify-desktop-node.mjs && node scripts/verify-language-pin.mjs && node scripts/verify-git-multi-sync.mjs && node scripts/verify-client-i18n.mjs && node scripts/verify-mutation-lock.mjs && node scripts/verify-prompt.mjs && node scripts/verify-conflict-config-guards.mjs && node scripts/verify-config-ranges.mjs && node scripts/verify-config-patch.mjs && node scripts/verify-tool-views.mjs && npx tsx scripts/verify-plugin-runtime.mjs && node scripts/verify-wechat-adapters.mjs && node scripts/verify-wechat-docs-seed.mjs && node scripts/verify-wechat-publish.mjs && npx tsx scripts/verify-tw-origin.mjs && node scripts/verify-frame-guards.mjs && npx tsx scripts/verify-ready-policy.mjs && npx tsx scripts/verify-frame-surface.mjs && npx tsx scripts/verify-editor-popup.mjs && npx tsx scripts/verify-status-cache.mjs && npx tsx scripts/verify-wiki-focus.mjs",
|
|
96
96
|
"verify:e2e": "node scripts/verify-auth.mjs && node scripts/verify-anon-username.mjs && node scripts/verify-tools.mjs && node scripts/verify-audit-fixes.mjs && node scripts/verify-git-resolve.mjs && node scripts/verify-resilience.mjs && node scripts/verify-seed-send-to-agent.mjs && node scripts/verify-seeds-admin.mjs && node scripts/verify-wiki-switch.mjs && node scripts/verify-wiki-farm-boot.mjs",
|
|
97
97
|
"verify:large": "node scripts/verify-large-wiki.mjs",
|
package/src/index-git.ts
CHANGED
|
@@ -58,8 +58,6 @@ export interface GitLayerDeps {
|
|
|
58
58
|
defaultPath: () => string
|
|
59
59
|
/** The multi-wiki control file (a registry write may be needed here). */
|
|
60
60
|
registryFile: () => string
|
|
61
|
-
/** Teardown-aware (v0.23.5): a reapply must not re-arm after dispose. */
|
|
62
|
-
isDisposed: () => boolean
|
|
63
61
|
}
|
|
64
62
|
|
|
65
63
|
export interface GitLayer {
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-tiddlywiki — host half: the ROUTE + ADMIN assembly stage.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `apply()` (v0.30.48) because it is a self-contained STAGE: the
|
|
5
|
+
* two `Deps` objects below are the whole surface the route table and the
|
|
6
|
+
* settings-panel admin routes consume, and they are built in one place from the
|
|
7
|
+
* plugin closure. Reading the entry point top-to-bottom now shows the lifecycle
|
|
8
|
+
* (farm → prompt/tools → startup task → routes), instead of the lifecycle
|
|
9
|
+
* interrupted by ~120 lines of accessor wiring.
|
|
10
|
+
*
|
|
11
|
+
* WHAT THIS MODULE DELIBERATELY DOES **NOT** OWN
|
|
12
|
+
* ----------------------------------------------
|
|
13
|
+
* - `disposed` stays in the entry point (its owner is the teardown effect and
|
|
14
|
+
* the startup task), so the one guard that needs it is passed IN as a
|
|
15
|
+
* predicate — `isDisposed` — rather than read from a second home.
|
|
16
|
+
* - The mutation lock: `createMutationLock()` is called by the caller and the
|
|
17
|
+
* SAME instance is handed to both `RouteDeps` and `AdminDeps` (v0.30.12: the
|
|
18
|
+
* plugin has exactly one lock, and it must be provably one — see
|
|
19
|
+
* scripts/verify-mutation-lock.mjs). This module never constructs one.
|
|
20
|
+
* - Order is NOT this module's business: `registerXxxRoutes()` below is called
|
|
21
|
+
* by the entry point at the exact position it used to occupy, right after the
|
|
22
|
+
* wiki views and right before the teardown effect. Nothing here reorders the
|
|
23
|
+
* lifecycle.
|
|
24
|
+
*
|
|
25
|
+
* @module dsh-tiddlywiki/index-routes
|
|
26
|
+
*/
|
|
27
|
+
import type { IncomingMessage } from 'node:http'
|
|
28
|
+
import {
|
|
29
|
+
registerRoutes,
|
|
30
|
+
type AgentPresetsFace,
|
|
31
|
+
type PermissionPresetsFace,
|
|
32
|
+
type SessionControllerFace,
|
|
33
|
+
type SessionPersistenceFace,
|
|
34
|
+
type SessionsFace,
|
|
35
|
+
type SessionQueryFace,
|
|
36
|
+
type WebServerFace,
|
|
37
|
+
type WorkspaceRegistryFace,
|
|
38
|
+
} from './host/routes.ts'
|
|
39
|
+
import {
|
|
40
|
+
registerAdminRoutes,
|
|
41
|
+
resolveTwRoot,
|
|
42
|
+
type AdminDeps,
|
|
43
|
+
type AdminWikisApplyResult,
|
|
44
|
+
type AdminWikisView,
|
|
45
|
+
} from './host/admin.ts'
|
|
46
|
+
import { ConfigStore, type PluginConfigShape } from './host/config.ts'
|
|
47
|
+
import type { GitFace } from './host/git.ts'
|
|
48
|
+
import { checkAllSeeds, runSeedById, removeSeedById } from './host/seeds.ts'
|
|
49
|
+
import { describePrompt, type PromptConfig } from './host/prompt.ts'
|
|
50
|
+
import type { RepoCommitters } from './host/repo-committers.ts'
|
|
51
|
+
import { normalizeWechatConfig, checkWechatReady, type WechatPublishRunner } from './host/wechat-publish.ts'
|
|
52
|
+
import { tiddlywikiToolSummary } from './host/tools.ts'
|
|
53
|
+
import {
|
|
54
|
+
DEFAULT_WIKI_ID,
|
|
55
|
+
applyWikiAction,
|
|
56
|
+
defaultEntry,
|
|
57
|
+
entryPath,
|
|
58
|
+
singleEntryRegistry,
|
|
59
|
+
writeRegistry,
|
|
60
|
+
type WikiRegistry,
|
|
61
|
+
} from './host/wiki-registry.ts'
|
|
62
|
+
import type { WikiLocation } from './host/wiki-location.ts'
|
|
63
|
+
import { writeLocationState } from './host/wiki-location.ts'
|
|
64
|
+
import { WikiInstance } from './host/wiki-instance.ts'
|
|
65
|
+
import { resolveAgentScope, stoppedWikiFromRequest, stoppedWikiMessage, targetRuntimeFor, type WikiFarm } from './host/wiki-farm.ts'
|
|
66
|
+
import { isSafeSessionId, setSessionScope } from './host/session-scope.ts'
|
|
67
|
+
import type { MutationLock } from './host/mutation-lock.ts'
|
|
68
|
+
import { restartAffectedWikis } from './index-wikis.ts'
|
|
69
|
+
import type { WikiViews } from './index-wikis.ts'
|
|
70
|
+
|
|
71
|
+
export interface RouteStageDeps {
|
|
72
|
+
/** The lazy webServer handle the caller injected. */
|
|
73
|
+
webServer: WebServerFace
|
|
74
|
+
/** The plugin context — host services are resolved from it lazily. */
|
|
75
|
+
get(name: string): unknown
|
|
76
|
+
/** One `GitFace` for the whole plugin. */
|
|
77
|
+
git: GitFace
|
|
78
|
+
/** One commit layer for the whole plugin (keyed by repository). */
|
|
79
|
+
repos: RepoCommitters
|
|
80
|
+
/** The running farm (undefined until the control file has been read). */
|
|
81
|
+
getFarm: () => WikiFarm<WikiInstance> | undefined
|
|
82
|
+
/** The folder the default wiki occupies, running or not. */
|
|
83
|
+
defaultPath: () => string
|
|
84
|
+
/** The `{root,name}` the default wiki currently occupies (running or not). */
|
|
85
|
+
defaultCurrentLocation: () => WikiLocation
|
|
86
|
+
/** What the CONTROL FILE says right now (synthesized when it does not exist). */
|
|
87
|
+
controlRegistryNow: () => WikiRegistry
|
|
88
|
+
/** Cache the registry the settings page just wrote (the control-file cache). */
|
|
89
|
+
setControlRegistry: (registry: WikiRegistry) => void
|
|
90
|
+
/** sessionId → wikiId; read/written by the composer's per-session selector. */
|
|
91
|
+
getSessionScopes: () => Record<string, string>
|
|
92
|
+
setSessionScopes: (next: Record<string, string>) => void
|
|
93
|
+
/** File the session scopes live in (outside every wiki). */
|
|
94
|
+
sessionScopeFile: string
|
|
95
|
+
/** The multi-wiki control file (a registry write goes here). */
|
|
96
|
+
registryFile: string
|
|
97
|
+
/** Pointer file the legacy single-wiki switch persists. */
|
|
98
|
+
locationStateFile: string
|
|
99
|
+
/** The cordis `config:` block, as the base layer of every wiki's config. */
|
|
100
|
+
baseShape: PluginConfigShape
|
|
101
|
+
/** The resolved cordis config (for the per-request fallbacks below). */
|
|
102
|
+
config: { note: { tag: string }; wechat?: unknown; ui: { sendToAgent: { enabled: boolean; token?: string } } }
|
|
103
|
+
/** Wiki location surface: `locationInfo` / switch / reset / the list view. */
|
|
104
|
+
wikiViews: WikiViews
|
|
105
|
+
/** The settings-page save hook: prompt + server tuning + git reapply. */
|
|
106
|
+
onConfigChanged: () => void
|
|
107
|
+
/** The saved prompt config of the TARGETED wiki (used when no draft is sent). */
|
|
108
|
+
effectivePromptFor: (req: IncomingMessage | undefined) => PromptConfig
|
|
109
|
+
/** ONE lock for the whole plugin (v0.30.12) — built by the caller. */
|
|
110
|
+
mutationLock: MutationLock
|
|
111
|
+
/** The WeChat publish runner (optional feature; owns the child process). */
|
|
112
|
+
wechatRunner: WechatPublishRunner
|
|
113
|
+
/** Teardown-aware: start → stop must not be accepted mid-dispose. */
|
|
114
|
+
isDisposed: () => boolean
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface RouteStage {
|
|
118
|
+
/** Release both route tables (the caller returns this from its disposer). */
|
|
119
|
+
dispose: () => void
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Build `RouteDeps` + `AdminDeps` and register both route tables. */
|
|
123
|
+
export function createRouteStage(deps: RouteStageDeps): RouteStage {
|
|
124
|
+
const { webServer: ws, git, repos, wikiViews } = deps
|
|
125
|
+
const { defaultPath, defaultCurrentLocation, locationInfo, switchWikiLocation, resetWikiLocation, buildWikisView } = wikiViews
|
|
126
|
+
|
|
127
|
+
// sessionController is a core host service; read it LAZILY per request (via
|
|
128
|
+
// the plugin root ctx) because it may be registered after webServer, and the
|
|
129
|
+
// agent routes must work whenever a request actually arrives.
|
|
130
|
+
const getSessionController = (): SessionControllerFace | undefined =>
|
|
131
|
+
deps.get('sessionController') as SessionControllerFace | undefined
|
|
132
|
+
const getWorkspaceRegistry = (): WorkspaceRegistryFace | undefined =>
|
|
133
|
+
deps.get('workspaceRegistry') as WorkspaceRegistryFace | undefined
|
|
134
|
+
// agentPresets (工作模式 roster) + sessionPersistence (per-session preset
|
|
135
|
+
// badges) are also core host services — resolve them lazily like the above.
|
|
136
|
+
const getAgentPresets = (): AgentPresetsFace | undefined =>
|
|
137
|
+
deps.get('agentPresets') as AgentPresetsFace | undefined
|
|
138
|
+
const getSessionPersistence = (): SessionPersistenceFace | undefined =>
|
|
139
|
+
deps.get('sessionPersistence') as SessionPersistenceFace | undefined
|
|
140
|
+
// permissionPresets (权限 preset roster for the picker + applying the
|
|
141
|
+
// chosen permission to new sessions) and the `sessions` in-memory store
|
|
142
|
+
// (the created live session handed to permissionPresets.set) are core host
|
|
143
|
+
// services — resolve them lazily like the ones above.
|
|
144
|
+
const getPermissionPresets = (): PermissionPresetsFace | undefined =>
|
|
145
|
+
deps.get('permissionPresets') as PermissionPresetsFace | undefined
|
|
146
|
+
const getSessions = (): SessionsFace | undefined =>
|
|
147
|
+
deps.get('sessions') as SessionsFace | undefined
|
|
148
|
+
// sessionQuery (会话日志查询,供「知识库」Tab 判定「本会话相关笔记」) 也是核心
|
|
149
|
+
// host 服务 —— 可选注入(本部署存在),懒解析。
|
|
150
|
+
const getSessionQuery = (): SessionQueryFace | undefined =>
|
|
151
|
+
deps.get('sessionQuery') as SessionQueryFace | undefined
|
|
152
|
+
|
|
153
|
+
// v0.28.0: the request decides WHICH wiki. `?wiki=<id>` (a running wiki) else
|
|
154
|
+
// the farm's default — resolved by the SHARED helper in host/wiki-farm.ts, so
|
|
155
|
+
// the host wiring and the verification harness cannot disagree about the
|
|
156
|
+
// selector (and an unknown id falls back instead of 404ing). Every accessor
|
|
157
|
+
// falls back to the cordis BASE when nothing is running, so a stopped farm
|
|
158
|
+
// degrades to "base defaults + 503 on writes" instead of throwing.
|
|
159
|
+
const target = (req: IncomingMessage): WikiInstance | undefined => targetRuntimeFor(deps.getFarm(), req)
|
|
160
|
+
/**
|
|
161
|
+
* Why this request cannot be served (v0.29.0): set when `?wiki=<id>` names a
|
|
162
|
+
* REGISTERED but STOPPED knowledge base. `targetRuntimeFor` deliberately
|
|
163
|
+
* returns undefined for that case instead of falling back to the default wiki
|
|
164
|
+
* (an unknown id still falls back — a stale bookmark must not 404), so every
|
|
165
|
+
* route that either 503s on a missing client or has a harmless-looking
|
|
166
|
+
* fallback (config / wiki path / prompt / status) can say WHICH wiki is down
|
|
167
|
+
* and what to do, instead of quietly acting on a different one.
|
|
168
|
+
*/
|
|
169
|
+
const targetProblem = (req: IncomingMessage): string | undefined => {
|
|
170
|
+
const entry = stoppedWikiFromRequest(deps.getFarm(), req)
|
|
171
|
+
return entry === undefined ? undefined : stoppedWikiMessage(entry)
|
|
172
|
+
}
|
|
173
|
+
const fallbackUi = WikiInstance.uiDefaultsFrom(deps.config as never)
|
|
174
|
+
const fallbackWechat = normalizeWechatConfig((deps.config as { wechat?: unknown }).wechat)
|
|
175
|
+
/** Used only while nothing runs: base defaults, no wiki tiddler to read. */
|
|
176
|
+
const idleConfig = new ConfigStore(deps.baseShape)
|
|
177
|
+
|
|
178
|
+
const disposeRoutes = registerRoutes({ webServer: ws }, {
|
|
179
|
+
mutationLock: deps.mutationLock,
|
|
180
|
+
server: (req) => target(req)?.server,
|
|
181
|
+
// The roster the GUI selector + the settings page read. It reports what
|
|
182
|
+
// the FARM serves right now (in single mode that is the one synthesized
|
|
183
|
+
// entry); the control file's full candidate list is the admin route's job.
|
|
184
|
+
wikiSummaries: (req) => {
|
|
185
|
+
const registry = deps.getFarm()?.registry
|
|
186
|
+
return {
|
|
187
|
+
mode: registry?.mode ?? 'single',
|
|
188
|
+
defaultId: registry?.defaultId ?? DEFAULT_WIKI_ID,
|
|
189
|
+
items: (registry?.wikis ?? []).map((entry) => {
|
|
190
|
+
const runtime = deps.getFarm()?.runtime(entry.id)
|
|
191
|
+
return {
|
|
192
|
+
id: entry.id,
|
|
193
|
+
label: entry.label,
|
|
194
|
+
status: runtime?.server.status().status ?? 'stopped',
|
|
195
|
+
agentVisible: entry.agentVisible,
|
|
196
|
+
autostart: entry.autostart,
|
|
197
|
+
running: runtime !== undefined,
|
|
198
|
+
path: entryPathOf(deps.getFarm(), entry.id),
|
|
199
|
+
// 每库图标(v0.28.4):侧边栏入口与设置页选择器共用这个来源。
|
|
200
|
+
icon: entry.icon,
|
|
201
|
+
}
|
|
202
|
+
}),
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
// The `/tw/<id>/…` form: the proxy resolves the child by NAME (it cannot
|
|
206
|
+
// use `target()`, whose `?wiki=` would be lost inside the iframe).
|
|
207
|
+
serverById: (id) => deps.getFarm()?.runtime(id)?.server,
|
|
208
|
+
wikiIds: () => deps.getFarm()?.registry.wikis.map((entry) => entry.id) ?? [],
|
|
209
|
+
getClient: (req) => target(req)?.client(),
|
|
210
|
+
git,
|
|
211
|
+
autoCommit: (req) => target(req)?.touchAutoCommit(),
|
|
212
|
+
noteDefaults: (req) => ({ tag: target(req)?.noteTag() ?? deps.config.note.tag }),
|
|
213
|
+
uiDefaults: (req) => target(req)?.uiDefaults() ?? fallbackUi,
|
|
214
|
+
langOf: (req) => target(req)?.uiLanguage() ?? (typeof deps.baseShape.uiLanguage === 'string' && deps.baseShape.uiLanguage.trim().length > 0 ? deps.baseShape.uiLanguage.trim() : 'zh'),
|
|
215
|
+
getWikiPath: (req) => target(req)?.path ?? defaultPath(),
|
|
216
|
+
targetProblem,
|
|
217
|
+
// Same helper as the agent tool (one implementation, two callers): a pull
|
|
218
|
+
// can change several knowledge bases that share one repository.
|
|
219
|
+
restartAffected: (_req, dir, changedFiles) => restartAffectedWikis({ farm: deps.getFarm, repoRootOf: (d) => repos.repoRootOf(d) }, dir, changedFiles),
|
|
220
|
+
// The composer's per-session selector (v0.28.0). Reading is a Map lookup;
|
|
221
|
+
// writing persists to session-scope.ts AND starts the wiki on demand — the
|
|
222
|
+
// tools resolve the scope SYNCHRONOUSLY and never start anything, so the
|
|
223
|
+
// selection itself has to bring the wiki up, or the very next tool call
|
|
224
|
+
// would have to refuse.
|
|
225
|
+
sessionScope: {
|
|
226
|
+
get: (sessionId: string) => {
|
|
227
|
+
const scopes = deps.getSessionScopes()
|
|
228
|
+
const resolution = resolveAgentScope(deps.getFarm(), scopes, sessionId)
|
|
229
|
+
const scopeId = scopes[sessionId]
|
|
230
|
+
return {
|
|
231
|
+
...(scopeId !== undefined ? { scope: scopeId } : {}),
|
|
232
|
+
...(resolution.entry !== undefined ? { resolved: { id: resolution.entry.id, label: resolution.entry.label } } : {}),
|
|
233
|
+
...(resolution.reason !== undefined ? { reason: resolution.reason } : {}),
|
|
234
|
+
// v0.29.0: the client needs to know whether "resolved" is a real
|
|
235
|
+
// answer (multi: this session's tools act on THAT wiki, so its cards'
|
|
236
|
+
// links must open it) or just the single install's only wiki (keep
|
|
237
|
+
// the pre-multi DOM, which renders no wiki attribute at all).
|
|
238
|
+
mode: deps.getFarm()?.registry.mode ?? 'single',
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
set: async (sessionId: string, wikiId: string | undefined) => {
|
|
242
|
+
if (!isSafeSessionId(sessionId)) throw new Error('会话 id 非法')
|
|
243
|
+
const farm = deps.getFarm()
|
|
244
|
+
if (wikiId !== undefined) {
|
|
245
|
+
const entry = farm?.registry.wikis.find((item) => item.id === wikiId)
|
|
246
|
+
if (entry === undefined) throw new Error(`知识库「${wikiId}」不在清单里`)
|
|
247
|
+
// `agentVisible: false` means "the agent never reaches this one"; a
|
|
248
|
+
// selector that allowed it would contradict the setting silently.
|
|
249
|
+
if (!entry.agentVisible) throw new Error(`知识库「${entry.label}」对 Agent 隐身,不能作为会话作用域`)
|
|
250
|
+
if (farm !== undefined && farm.runtime(entry.id) === undefined) await farm.startEntry(entry)
|
|
251
|
+
}
|
|
252
|
+
await setSessionScope(sessionId, wikiId, deps.sessionScopeFile)
|
|
253
|
+
const next = { ...deps.getSessionScopes() }
|
|
254
|
+
if (wikiId === undefined) delete next[sessionId]
|
|
255
|
+
else next[sessionId] = wikiId
|
|
256
|
+
deps.setSessionScopes(next)
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
getSessionController,
|
|
260
|
+
getWorkspaceRegistry,
|
|
261
|
+
getAgentPresets,
|
|
262
|
+
getSessionPersistence,
|
|
263
|
+
getPermissionPresets,
|
|
264
|
+
getSessions,
|
|
265
|
+
getSessionQuery,
|
|
266
|
+
sendToAgentEnabled: (req) => target(req)?.sendToAgent().enabled ?? deps.config.ui.sendToAgent.enabled,
|
|
267
|
+
sendToAgentToken: (req) => target(req)?.sendToAgent().token ?? (deps.config.ui.sendToAgent.token ?? ''),
|
|
268
|
+
// 公众号发布(v0.23.3,可选功能):config 每请求重读(开关/adapter/token
|
|
269
|
+
// 保存即生效,且每个知识库各有一份);就绪探测每次都真跑一次
|
|
270
|
+
// `opencli --version`(几百毫秒),只在按钮预检与每次起任务前发生,
|
|
271
|
+
// 不做缓存以免装完 adapter 还要等 TTL。
|
|
272
|
+
wechatConfig: (req) => target(req)?.wechatConfig() ?? fallbackWechat,
|
|
273
|
+
wechatRunner: () => deps.wechatRunner,
|
|
274
|
+
wechatReady: async (req) => {
|
|
275
|
+
const cfg = target(req)?.wechatConfig() ?? fallbackWechat
|
|
276
|
+
return checkWechatReady({ enabled: cfg.enabled, command: cfg.command })
|
|
277
|
+
},
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
const adminDeps: AdminDeps = {
|
|
281
|
+
mutationLock: deps.mutationLock,
|
|
282
|
+
server: (req) => target(req)?.server,
|
|
283
|
+
getClient: (req) => target(req)?.client(),
|
|
284
|
+
getWikiPath: (req) => target(req)?.path ?? defaultPath(),
|
|
285
|
+
targetProblem,
|
|
286
|
+
twRoot: resolveTwRoot,
|
|
287
|
+
config: (req) => target(req)?.config ?? idleConfig,
|
|
288
|
+
onConfigChanged: deps.onConfigChanged,
|
|
289
|
+
// No draft → the SAVED config of the TARGETED wiki (what is injected right
|
|
290
|
+
// now); with a draft → the settings form's unsaved values (v0.22.7),
|
|
291
|
+
// through the same builder.
|
|
292
|
+
getPrompt: (draft, req) =>
|
|
293
|
+
describePrompt(draft ?? deps.effectivePromptFor(req), tiddlywikiToolSummary()),
|
|
294
|
+
// Runtime wiki location (v0.22.0): read the current folder + how it was
|
|
295
|
+
// decided, switch to another one, or drop back to the cordis default.
|
|
296
|
+
wiki: {
|
|
297
|
+
info: locationInfo,
|
|
298
|
+
switch: switchWikiLocation,
|
|
299
|
+
reset: resetWikiLocation,
|
|
300
|
+
},
|
|
301
|
+
// The knowledge-base LIST (v0.28.0): `info` reports the CONTROL FILE; every
|
|
302
|
+
// action is validated by the pure `applyWikiAction` and persisted BEFORE
|
|
303
|
+
// the farm reconciles — the file is the user's intent, and a reconcile
|
|
304
|
+
// failure is reported per wiki instead of silently discarding the edit.
|
|
305
|
+
wikis: {
|
|
306
|
+
info: async (): Promise<AdminWikisView> => buildWikisView(),
|
|
307
|
+
apply: async (body: unknown): Promise<AdminWikisApplyResult> => {
|
|
308
|
+
if (deps.isDisposed()) return { ok: false, error: '插件正在卸载,已取消修改' }
|
|
309
|
+
// RUNTIME actions first (v0.28.0): `start`/`stop` do not change the list,
|
|
310
|
+
// they change what is running. They live here rather than in the pure
|
|
311
|
+
// `applyWikiAction` because they touch processes, not configuration —
|
|
312
|
+
// and the GUI needs them: opening a wiki's panel must be able to bring
|
|
313
|
+
// a stopped knowledge base up.
|
|
314
|
+
const farm = deps.getFarm()
|
|
315
|
+
const runtimeAction = (body as { action?: unknown } | null)?.action
|
|
316
|
+
if (runtimeAction === 'start' || runtimeAction === 'stop') {
|
|
317
|
+
if (farm === undefined) return { ok: false, error: '插件尚未就绪,请稍后再试' }
|
|
318
|
+
const id = (body as { id?: unknown }).id
|
|
319
|
+
const entry = typeof id === 'string' ? farm.registry.wikis.find((item) => item.id === id.trim().toLowerCase()) : undefined
|
|
320
|
+
if (entry === undefined) return { ok: false, error: `知识库「${String(id)}」不在清单里` }
|
|
321
|
+
const change = { started: [] as string[], stopped: [] as string[], updated: [] as string[], running: farm.runningIds(), errors: [] as Array<{ id: string; message: string }> }
|
|
322
|
+
if (runtimeAction === 'start') {
|
|
323
|
+
if (farm.runtime(entry.id) === undefined) {
|
|
324
|
+
await farm.startEntry(entry)
|
|
325
|
+
change.started.push(entry.id)
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
await farm.stopEntry(entry.id)
|
|
329
|
+
change.stopped.push(entry.id)
|
|
330
|
+
}
|
|
331
|
+
change.running = farm.runningIds()
|
|
332
|
+
return { ok: true, info: buildWikisView(), change }
|
|
333
|
+
}
|
|
334
|
+
const action = applyWikiAction(deps.controlRegistryNow(), body)
|
|
335
|
+
if (action.registry === undefined) return { ok: false, error: action.error ?? '动作被拒绝' }
|
|
336
|
+
const registry = action.registry
|
|
337
|
+
try {
|
|
338
|
+
await writeRegistry(registry, deps.registryFile)
|
|
339
|
+
} catch (err) {
|
|
340
|
+
return { ok: false, error: `清单写入失败:${err instanceof Error ? err.message : String(err)}` }
|
|
341
|
+
}
|
|
342
|
+
deps.setControlRegistry(registry)
|
|
343
|
+
// Reconcile. multi → the farm runs the whole list. single → it must
|
|
344
|
+
// keep serving the ONE wiki the legacy pointer names, so the
|
|
345
|
+
// synthesized run-registry is used and the pointer is kept in step
|
|
346
|
+
// (otherwise a restart in single mode would land somewhere else).
|
|
347
|
+
const single = defaultEntry(registry)
|
|
348
|
+
const runRegistry = registry.mode === 'multi'
|
|
349
|
+
? registry
|
|
350
|
+
: singleEntryRegistry(single === undefined ? defaultCurrentLocation() : { root: single.root, name: single.name }, DEFAULT_WIKI_ID, 'single')
|
|
351
|
+
const change = farm === undefined
|
|
352
|
+
? { started: [], stopped: [], updated: [], running: [], errors: [] }
|
|
353
|
+
: await farm.apply(runRegistry)
|
|
354
|
+
if (registry.mode === 'single' && single !== undefined) {
|
|
355
|
+
await writeLocationState({ root: single.root, name: single.name }, deps.locationStateFile).catch(() => undefined)
|
|
356
|
+
}
|
|
357
|
+
return { ok: true, info: buildWikisView(), change }
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
seeds: {
|
|
361
|
+
// Tool summaries feed the GENERATED seed content (the doc note's tool
|
|
362
|
+
// list, v0.22.0) — pass them everywhere the registry can be re-run.
|
|
363
|
+
checkAll: async (c) => checkAllSeeds({ client: c, tools: tiddlywikiToolSummary() }),
|
|
364
|
+
run: async (c, id, force) => runSeedById({ client: c, tools: tiddlywikiToolSummary() }, id, force),
|
|
365
|
+
remove: async (c, id) => removeSeedById({ client: c }, id),
|
|
366
|
+
},
|
|
367
|
+
}
|
|
368
|
+
const disposeAdmin = registerAdminRoutes({ webServer: ws }, adminDeps)
|
|
369
|
+
return {
|
|
370
|
+
dispose: () => {
|
|
371
|
+
disposeRoutes()
|
|
372
|
+
disposeAdmin()
|
|
373
|
+
},
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** The folder a registered wiki occupies, running or not. */
|
|
378
|
+
function entryPathOf(farm: WikiFarm<WikiInstance> | undefined, id: string): string {
|
|
379
|
+
const entry = farm?.registry.wikis.find((item) => item.id === id)
|
|
380
|
+
return entry === undefined ? '' : entryPath(entry)
|
|
381
|
+
}
|