@twinsearth/fang-dsh-plugin 0.0.0-stage → 3.3.4
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/CLA.md +79 -0
- package/LICENSE-COMMERCIAL +78 -0
- package/LICENSE-CORE +661 -0
- package/LICENSE-SDK +21 -0
- package/LICENSES/cordis-MIT.txt +39 -0
- package/NOTICE +95 -0
- package/README.md +158 -2
- package/client.js +7287 -0
- package/cordis.patch.yml +163 -0
- package/defaults/umbrella-rules.yaml +113 -0
- package/docs/CONTRACT-INTEROP.md +77 -0
- package/docs/CRASH-SAFETY.md +69 -0
- package/docs/DECISION-RELAY.md +46 -0
- package/docs/DSH-ARCHITECTURE-MAP.md +73 -0
- package/docs/FORK-SEED.md +64 -0
- package/docs/MAIN-CITY-GATE.md +58 -0
- package/docs/OVERLAY-MASTER-SWITCH.md +35 -0
- package/docs/PACKAGE-RENAME-v3.3.3.md +40 -0
- package/docs/PM-DECISION-POLICY.md +61 -0
- package/docs/WORKFLOW-UI.md +86 -0
- package/icon.svg +8 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +71 -4
- package/sdk/README.md +65 -0
- package/sdk/examples/status-badge/README.md +16 -0
- package/sdk/examples/status-badge/client.js +60 -0
- package/sdk/examples/status-badge/cordis.patch.yml +7 -0
- package/sdk/examples/status-badge/index.js +12 -0
- package/sdk/examples/status-badge/package.json +24 -0
- package/sdk/templates/plugin/README.md +36 -0
- package/sdk/templates/plugin/client.js +35 -0
- package/sdk/templates/plugin/cordis.patch.yml +8 -0
- package/sdk/templates/plugin/icon.svg +5 -0
- package/sdk/templates/plugin/index.js +23 -0
- package/sdk/templates/plugin/locale/en.json +6 -0
- package/sdk/templates/plugin/package.json +36 -0
- package/src/compose.mjs +67 -0
- package/src/contract/VENDOR.json +16 -0
- package/src/contract/client.js +93 -0
- package/src/contract/constants.js +94 -0
- package/src/contract/envelope.js +83 -0
- package/src/contract/index.js +20 -0
- package/src/contract/negotiate.js +106 -0
- package/src/contract/relay.js +223 -0
- package/src/contract/server.js +77 -0
- package/src/contract/transports.js +319 -0
- package/src/index.mjs +11 -0
- package/src/kernel/core.mjs +100 -0
- package/src/kernel/extensions.mjs +61 -0
- package/src/kernel/fsx.mjs +349 -0
- package/src/kernel/index.mjs +21 -0
- package/src/kernel/ledger.mjs +94 -0
- package/src/kernel/paths.mjs +182 -0
- package/src/kernel/role.mjs +68 -0
- package/src/kernel/rounds.mjs +172 -0
- package/src/kernel/schema.mjs +5 -0
- package/src/kernel/types.mjs +266 -0
- package/src/kernel/vendor/cosmokit.mjs +421 -0
- package/src/kernel/vendor/schemastery.mjs +638 -0
- package/src/lib/adapter-client.mjs +383 -0
- package/src/lib/adapter-protocol.mjs +243 -0
- package/src/lib/ai-secret-store.mjs +202 -0
- package/src/lib/boot-guard.mjs +186 -0
- package/src/lib/branch-store.mjs +84 -0
- package/src/lib/branch.mjs +74 -0
- package/src/lib/bridge-client.mjs +133 -0
- package/src/lib/broker-adapter.mjs +67 -0
- package/src/lib/broker.mjs +45 -0
- package/src/lib/business-producers.mjs +109 -0
- package/src/lib/command-cache.mjs +209 -0
- package/src/lib/compat.mjs +170 -0
- package/src/lib/composer-journal.mjs +128 -0
- package/src/lib/container/index.mjs +806 -0
- package/src/lib/decision-events.mjs +129 -0
- package/src/lib/decision-ingest-client.mjs +41 -0
- package/src/lib/decision-outbox.mjs +129 -0
- package/src/lib/decision-policy.mjs +52 -0
- package/src/lib/default-essence.mjs +61 -0
- package/src/lib/desktop-launch.mjs +138 -0
- package/src/lib/essence.mjs +1211 -0
- package/src/lib/fork-client.mjs +410 -0
- package/src/lib/fork-config-lock.mjs +166 -0
- package/src/lib/fork-seed.mjs +304 -0
- package/src/lib/fork-sessions.mjs +183 -0
- package/src/lib/fork-supervision-settings.mjs +58 -0
- package/src/lib/fork-supervisor.mjs +122 -0
- package/src/lib/fs-guard.mjs +28 -0
- package/src/lib/http-auth.mjs +162 -0
- package/src/lib/idle-audit.mjs +178 -0
- package/src/lib/interaction-poke.mjs +61 -0
- package/src/lib/main-gate.mjs +579 -0
- package/src/lib/mirror-etag.mjs +92 -0
- package/src/lib/mirror-official-source.mjs +93 -0
- package/src/lib/mirror.mjs +478 -0
- package/src/lib/mother-link.mjs +156 -0
- package/src/lib/mutex.mjs +31 -0
- package/src/lib/permissions.mjs +115 -0
- package/src/lib/pnpm-store.mjs +189 -0
- package/src/lib/preferences.mjs +212 -0
- package/src/lib/producer-contract.mjs +162 -0
- package/src/lib/reflow.mjs +41 -0
- package/src/lib/remote.mjs +408 -0
- package/src/lib/restart-contract.mjs +752 -0
- package/src/lib/roots.mjs +30 -0
- package/src/lib/rule-registry.mjs +1127 -0
- package/src/lib/runtime-capture.mjs +307 -0
- package/src/lib/semaphore.mjs +34 -0
- package/src/lib/session-bundle.mjs +138 -0
- package/src/lib/session-format.mjs +672 -0
- package/src/lib/sse-parser.mjs +102 -0
- package/src/lib/ssrf-guard.mjs +170 -0
- package/src/lib/status-store.mjs +257 -0
- package/src/lib/task-handle.mjs +622 -0
- package/src/lib/ui-actions.mjs +55 -0
- package/src/lib/umbrella.mjs +1661 -0
- package/src/lib/upgrade-coordinator.mjs +107 -0
- package/src/lib/upgrade-status.mjs +43 -0
- package/src/plugins/adapter.mjs +1468 -0
- package/src/plugins/approver.mjs +1210 -0
- package/src/plugins/auditor.mjs +330 -0
- package/src/plugins/buffer-keeper.mjs +522 -0
- package/src/plugins/decision-center.mjs +2581 -0
- package/src/plugins/forker.mjs +1610 -0
- package/src/plugins/healer.mjs +702 -0
- package/src/plugins/hot-reloader.mjs +274 -0
- package/src/plugins/imports.mjs +247 -0
- package/src/plugins/orchestrator.mjs +3938 -0
- package/src/plugins/restart-guard.mjs +1022 -0
- package/src/plugins/router.mjs +714 -0
- package/src/plugins/scorer.mjs +661 -0
- package/src/plugins/session-bridge.mjs +166 -0
- package/src/plugins/session-catalog.mjs +771 -0
- package/src/plugins/session-router.mjs +125 -0
- package/src/plugins/session-sync.mjs +171 -0
- package/src/plugins/umbrella.mjs +419 -0
- package/tools/boot-guard.mjs +95 -0
- package/tools/decision-ingest.mjs +53 -0
- package/tools/desktop-launch.mjs +71 -0
- package/tools/fangctl.mjs +408 -0
- package/tools/gate/main-city.mjs +204 -0
- package/tools/mcp/server.mjs +121 -0
- package/tools/mcp/tools.mjs +65 -0
- package/tools/recover-queue.mjs +133 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// dsh-Fang v3.0.3 — 客户端升级后的兼容性门禁(compat gate)。
|
|
3
|
+
//
|
|
4
|
+
// 触发时机:母体镜像(客户端版本)升级完成后。
|
|
5
|
+
// 目的:更新客户端版本后,默认不启用子插件;先验证
|
|
6
|
+
// * 全部分子插件模块可导入且导出 apply(当前 16 个,后续增多自动纳入);
|
|
7
|
+
// * 内核入口可导入;
|
|
8
|
+
// * 路由契约未破(数量 + 必需路径);
|
|
9
|
+
// 通过 → 允许启用子插件并继续自动适配;失败 → 保持子插件关闭,
|
|
10
|
+
// 写入 compat-status.json,等开发者更新插件版本后再次检查。
|
|
11
|
+
//
|
|
12
|
+
// 纯逻辑 + 动态 import;测试可注入 importModule 与 routePaths。
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs'
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
import { pathToFileURL } from 'node:url'
|
|
17
|
+
|
|
18
|
+
import { ensureDir, readJson, writeJsonAtomic } from '../kernel/index.mjs'
|
|
19
|
+
|
|
20
|
+
export const COMPAT_STATUS_FILE = 'compat-status.json'
|
|
21
|
+
export const COMPAT_KERNEL_ENTRY = 'src/kernel/index.mjs'
|
|
22
|
+
export const COMPAT_PLUGIN_DIR = 'src/plugins'
|
|
23
|
+
export const COMPAT_EXCLUDED_PLUGINS = Object.freeze(['imports.mjs'])
|
|
24
|
+
/** 路由契约:客户端升级后仍必须存在的核心路由(诊断/升级/镜像)。 */
|
|
25
|
+
export const REQUIRED_ROUTE_PATHS = Object.freeze([
|
|
26
|
+
'/fang/api/status',
|
|
27
|
+
'/fang/api/auth-token',
|
|
28
|
+
'/fang/api/mirror/status',
|
|
29
|
+
'/fang/api/mirror/upgrade',
|
|
30
|
+
'/fang/api/mirror/recompute-status',
|
|
31
|
+
'/fang/api/compat',
|
|
32
|
+
'/fang/api/essence/list',
|
|
33
|
+
'/fang/api/upgrade/status',
|
|
34
|
+
'/fang/api/tasks/branches',
|
|
35
|
+
'/fang/api/reflow/policy',
|
|
36
|
+
'/fang/api/composer/journal',
|
|
37
|
+
])
|
|
38
|
+
export const DEFAULT_EXPECTED_ROUTE_COUNT = 85
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 列出需要做导入检查的插件模块文件(排序稳定)。
|
|
42
|
+
* @param {string} rootDir 仓库/安装根目录
|
|
43
|
+
* @returns {string[]} 相对 rootDir 的路径
|
|
44
|
+
*/
|
|
45
|
+
export function listCompatModules(rootDir) {
|
|
46
|
+
const dir = path.join(rootDir, COMPAT_PLUGIN_DIR)
|
|
47
|
+
const files = fs.readdirSync(dir, { withFileTypes: true })
|
|
48
|
+
.filter(entry => entry.isFile() && entry.name.endsWith('.mjs') && !COMPAT_EXCLUDED_PLUGINS.includes(entry.name))
|
|
49
|
+
.map(entry => entry.name)
|
|
50
|
+
.sort()
|
|
51
|
+
return files.map(file => `${COMPAT_PLUGIN_DIR}/${file}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 运行兼容性门禁。
|
|
56
|
+
* @param {{rootDir: string, expectedRouteCount?: number, routePaths?: string[],
|
|
57
|
+
* requiredRoutes?: string[], importModule?: (url: string) => Promise<any>}} options
|
|
58
|
+
* @returns {Promise<{ok:boolean, checkedAt:number, moduleCount:number, routeCount:number,
|
|
59
|
+
* expectedRouteCount:number, findings:Array<{code:string, severity:'error'|'warning', message:string}>}>}
|
|
60
|
+
*/
|
|
61
|
+
export async function runCompatGate(options) {
|
|
62
|
+
const rootDir = String(options?.rootDir ?? '')
|
|
63
|
+
if (rootDir.length === 0) throw new Error('runCompatGate: rootDir required')
|
|
64
|
+
const importModule = typeof options?.importModule === 'function'
|
|
65
|
+
? options.importModule
|
|
66
|
+
: (url) => import(url)
|
|
67
|
+
const expectedRouteCount = Math.max(0, Math.floor(Number(options?.expectedRouteCount) || DEFAULT_EXPECTED_ROUTE_COUNT))
|
|
68
|
+
const requiredRoutes = Array.isArray(options?.requiredRoutes) ? options.requiredRoutes : [...REQUIRED_ROUTE_PATHS]
|
|
69
|
+
const routePaths = Array.isArray(options?.routePaths) ? options.routePaths : null
|
|
70
|
+
/** @type {Array<{code:string, severity:'error'|'warning', message:string}>} */
|
|
71
|
+
const findings = []
|
|
72
|
+
|
|
73
|
+
// 1) 内核入口可导入
|
|
74
|
+
try {
|
|
75
|
+
await importModule(pathToFileURL(path.join(rootDir, COMPAT_KERNEL_ENTRY)).href)
|
|
76
|
+
} catch (error) {
|
|
77
|
+
findings.push({ code: 'kernel-import-failed', severity: 'error', message: `内核入口导入失败:${errorText(error)}` })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 2) 每个子插件可导入且导出 apply(模块数量即“当前 16 个后续更多”)
|
|
81
|
+
let moduleCount = 0
|
|
82
|
+
let moduleFiles = []
|
|
83
|
+
try {
|
|
84
|
+
moduleFiles = listCompatModules(rootDir)
|
|
85
|
+
} catch (error) {
|
|
86
|
+
findings.push({ code: 'plugin-dir-unreadable', severity: 'error', message: `插件目录不可读:${errorText(error)}` })
|
|
87
|
+
}
|
|
88
|
+
for (const relative of moduleFiles) {
|
|
89
|
+
try {
|
|
90
|
+
const mod = await importModule(pathToFileURL(path.join(rootDir, relative)).href)
|
|
91
|
+
moduleCount += 1
|
|
92
|
+
if (typeof mod?.apply !== 'function') {
|
|
93
|
+
findings.push({ code: 'plugin-apply-missing', severity: 'error', message: `${relative} 未导出 apply()` })
|
|
94
|
+
}
|
|
95
|
+
if (typeof mod?.name !== 'string' || mod.name.length === 0) {
|
|
96
|
+
findings.push({ code: 'plugin-name-missing', severity: 'warning', message: `${relative} 未导出 name` })
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
findings.push({ code: 'plugin-import-failed', severity: 'error', message: `${relative} 导入失败:${errorText(error)}` })
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 3) 路由契约:数量 + 必需路径
|
|
104
|
+
const routeCount = routePaths === null ? expectedRouteCount : routePaths.length
|
|
105
|
+
// 只把「路由凭空少了」当错误:新增路由(例如带进程 token 的连接信息)不应
|
|
106
|
+
// 让整个子插件集被关掉;数量增长记一条 warning,保持可见但不拦。
|
|
107
|
+
if (routePaths !== null && routeCount < expectedRouteCount) {
|
|
108
|
+
findings.push({
|
|
109
|
+
code: 'route-count-mismatch', severity: 'error',
|
|
110
|
+
message: `路由数量 ${routeCount} < 期望 ${expectedRouteCount}`,
|
|
111
|
+
})
|
|
112
|
+
} else if (routePaths !== null && routeCount > expectedRouteCount) {
|
|
113
|
+
findings.push({
|
|
114
|
+
code: 'route-count-growth', severity: 'warning',
|
|
115
|
+
message: `路由数量 ${routeCount} > 期望 ${expectedRouteCount}(新增路由,非错误)`,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
if (routePaths !== null) {
|
|
119
|
+
for (const required of requiredRoutes) {
|
|
120
|
+
if (!routePaths.includes(required)) {
|
|
121
|
+
findings.push({ code: 'route-missing', severity: 'error', message: `缺少必需路由 ${required}` })
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const ok = !findings.some(finding => finding.severity === 'error')
|
|
127
|
+
return {
|
|
128
|
+
ok,
|
|
129
|
+
checkedAt: Date.now(),
|
|
130
|
+
moduleCount,
|
|
131
|
+
routeCount,
|
|
132
|
+
expectedRouteCount,
|
|
133
|
+
findings,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 把门禁结果落盘;`subPluginsEnabled` 是唯一允许启用子插件的判据。 */
|
|
138
|
+
export function writeCompatStatus(stateDir, status) {
|
|
139
|
+
ensureDir(stateDir)
|
|
140
|
+
const payload = {
|
|
141
|
+
ok: status?.ok === true,
|
|
142
|
+
subPluginsEnabled: status?.ok === true,
|
|
143
|
+
checkedAt: Number(status?.checkedAt) || Date.now(),
|
|
144
|
+
moduleCount: Number(status?.moduleCount) || 0,
|
|
145
|
+
routeCount: Number(status?.routeCount) || 0,
|
|
146
|
+
expectedRouteCount: Number(status?.expectedRouteCount) || DEFAULT_EXPECTED_ROUTE_COUNT,
|
|
147
|
+
findings: Array.isArray(status?.findings) ? status.findings : [],
|
|
148
|
+
}
|
|
149
|
+
writeJsonAtomic(path.join(stateDir, COMPAT_STATUS_FILE), payload)
|
|
150
|
+
return payload
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 读取门禁状态;从未检查过时返回 subPluginsEnabled=false(安全默认)。 */
|
|
154
|
+
export function readCompatStatus(stateDir) {
|
|
155
|
+
const raw = readJson(path.join(stateDir, COMPAT_STATUS_FILE), null)
|
|
156
|
+
if (raw === null || typeof raw !== 'object') {
|
|
157
|
+
return {
|
|
158
|
+
ok: false, subPluginsEnabled: false, checkedAt: null,
|
|
159
|
+
moduleCount: 0, routeCount: 0, expectedRouteCount: DEFAULT_EXPECTED_ROUTE_COUNT,
|
|
160
|
+
findings: [], reason: 'not-checked',
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { ...raw, subPluginsEnabled: raw.subPluginsEnabled === true }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function errorText(error) {
|
|
167
|
+
return String(error instanceof Error ? error.message : error)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export const _internals = Object.freeze({ errorText })
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Composer journal: persist the user's unsent draft and queued-message mirror so a
|
|
3
|
+
// restart/crash can offer recovery. It never sends anything by itself.
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
|
|
7
|
+
const MAX_DRAFT = 100_000
|
|
8
|
+
const MAX_QUEUE = 100
|
|
9
|
+
const MAX_ITEM = 4_000
|
|
10
|
+
// v3.1.21:队列「必丢」根因之一是所有会话挤在同一个 key(后者覆盖前者)。
|
|
11
|
+
// 新增 append-only 历史文件:任何一次含队列的保存都会追加一条快照,
|
|
12
|
+
// 即使主 journal 被覆盖,历史里仍能找回来。
|
|
13
|
+
const MAX_HISTORY = 500
|
|
14
|
+
|
|
15
|
+
function cleanText(value, limit) {
|
|
16
|
+
return String(value ?? '').replace(/\0/g, '').slice(0, limit)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class ComposerJournal {
|
|
20
|
+
constructor(dataDir) {
|
|
21
|
+
this.file = path.join(dataDir, 'composer-journal.json')
|
|
22
|
+
this.historyFile = path.join(dataDir, 'composer-journal-history.jsonl')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
load() {
|
|
26
|
+
try {
|
|
27
|
+
const raw = JSON.parse(fs.readFileSync(this.file, 'utf8'))
|
|
28
|
+
return raw && typeof raw === 'object' ? raw : {}
|
|
29
|
+
} catch { return {} }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
saveFile(all) {
|
|
33
|
+
fs.mkdirSync(path.dirname(this.file), { recursive: true })
|
|
34
|
+
const tmp = this.file + '.tmp'
|
|
35
|
+
fs.writeFileSync(tmp, JSON.stringify(all, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 })
|
|
36
|
+
fs.renameSync(tmp, this.file)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get(key) {
|
|
40
|
+
const entry = this.load()[String(key)]
|
|
41
|
+
return entry && typeof entry === 'object' ? entry : null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** All recoverable journal entries, newest first (bounded by the same 100-session cap). */
|
|
45
|
+
list() {
|
|
46
|
+
const all = this.load()
|
|
47
|
+
return Object.values(all)
|
|
48
|
+
.filter((entry) => entry && typeof entry === 'object')
|
|
49
|
+
.sort((a, b) => String(b?.updatedAt || '').localeCompare(String(a?.updatedAt || '')))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
save(key, input = {}) {
|
|
53
|
+
const id = cleanText(key, 200)
|
|
54
|
+
if (!id) throw new Error('journal-key-required')
|
|
55
|
+
const queue = Array.isArray(input.queue)
|
|
56
|
+
? input.queue.slice(-MAX_QUEUE).map((item) => ({
|
|
57
|
+
text: cleanText(item?.text, MAX_ITEM),
|
|
58
|
+
at: Number(item?.at) || Date.now(),
|
|
59
|
+
})).filter((item) => item.text.trim())
|
|
60
|
+
: []
|
|
61
|
+
const entry = {
|
|
62
|
+
key: id,
|
|
63
|
+
draft: cleanText(input.draft, MAX_DRAFT),
|
|
64
|
+
queue,
|
|
65
|
+
path: cleanText(input.path, 500),
|
|
66
|
+
sessionKey: cleanText(input.sessionKey, 200),
|
|
67
|
+
updatedAt: new Date().toISOString(),
|
|
68
|
+
}
|
|
69
|
+
const all = this.load()
|
|
70
|
+
all[id] = entry
|
|
71
|
+
// Keep the journal bounded: 100 sessions is plenty for recovery.
|
|
72
|
+
const keys = Object.keys(all)
|
|
73
|
+
if (keys.length > 100) {
|
|
74
|
+
keys.sort((a, b) => String(all[a]?.updatedAt || '').localeCompare(String(all[b]?.updatedAt || '')))
|
|
75
|
+
for (const stale of keys.slice(0, keys.length - 100)) delete all[stale]
|
|
76
|
+
}
|
|
77
|
+
this.saveFile(all)
|
|
78
|
+
this.appendHistory({ ...entry, sessionKey: cleanText(input.sessionKey, 200) || entry.key })
|
|
79
|
+
return entry
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 追加一条历史快照(只在有队列内容时写,避免草稿噪音)。 */
|
|
83
|
+
appendHistory(entry) {
|
|
84
|
+
try {
|
|
85
|
+
const queue = Array.isArray(entry?.queue) ? entry.queue : []
|
|
86
|
+
if (queue.length === 0) return
|
|
87
|
+
fs.mkdirSync(path.dirname(this.historyFile), { recursive: true })
|
|
88
|
+
fs.appendFileSync(this.historyFile, JSON.stringify({
|
|
89
|
+
key: entry.key,
|
|
90
|
+
queue,
|
|
91
|
+
draft: entry.draft || "",
|
|
92
|
+
path: entry.path || "",
|
|
93
|
+
sessionKey: entry.sessionKey || "",
|
|
94
|
+
updatedAt: entry.updatedAt,
|
|
95
|
+
}) + "\n", { encoding: "utf8", mode: 0o600 })
|
|
96
|
+
// 裁剪:超过上限时重写最后 MAX_HISTORY 行
|
|
97
|
+
const raw = fs.readFileSync(this.historyFile, "utf8").trim().split("\n").filter(Boolean)
|
|
98
|
+
if (raw.length > MAX_HISTORY) {
|
|
99
|
+
fs.writeFileSync(this.historyFile, raw.slice(-MAX_HISTORY).join("\n") + "\n", { encoding: "utf8", mode: 0o600 })
|
|
100
|
+
}
|
|
101
|
+
} catch { /* 历史写入失败不影响主 journal */ }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 读取历史快照(newest first),可按 sessionKey 过滤。 */
|
|
105
|
+
history({ limit = 50, sessionKey = "" } = {}) {
|
|
106
|
+
try {
|
|
107
|
+
const raw = fs.readFileSync(this.historyFile, "utf8").trim().split("\n").filter(Boolean)
|
|
108
|
+
const items = []
|
|
109
|
+
for (const line of raw) {
|
|
110
|
+
try {
|
|
111
|
+
const entry = JSON.parse(line)
|
|
112
|
+
if (sessionKey && String(entry?.sessionKey || "") !== String(sessionKey)) continue
|
|
113
|
+
items.push(entry)
|
|
114
|
+
} catch { /* 跳过坏行 */ }
|
|
115
|
+
}
|
|
116
|
+
return items.slice(-Math.max(1, Number(limit) || 50)).reverse()
|
|
117
|
+
} catch { return [] }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
remove(key) {
|
|
121
|
+
const all = this.load()
|
|
122
|
+
const id = String(key)
|
|
123
|
+
if (!all[id]) return false
|
|
124
|
+
delete all[id]
|
|
125
|
+
this.saveFile(all)
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
}
|