@deployxai/dxc 0.1.0
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/README.md +131 -0
- package/dist/chunks/chunk-I6VZLNRZ.js +2118 -0
- package/dist/chunks/chunk-XIHX5YAF.js +16391 -0
- package/dist/chunks/knowledge-Q6MHPG6I.js +1248 -0
- package/dist/chunks/monitor-VPRVRQIS.js +694 -0
- package/dist/index.js +32367 -0
- package/docs/00-project-context.md +125 -0
- package/docs/01-north-star-architecture.md +234 -0
- package/docs/02-mvp-technical-design.md +553 -0
- package/docs/03-domain-state-api.md +599 -0
- package/docs/04-security-and-operations.md +413 -0
- package/docs/05-delivery-plan.md +407 -0
- package/docs/README.md +44 -0
- package/docs/decisions/0001-initial-architecture.md +57 -0
- package/docs/decisions/0002-mongodb-environment-boundary.md +42 -0
- package/docs/decisions/0003-staged-production-topology.md +33 -0
- package/docs/decisions/0004-local-first-agent-research-runtime.md +71 -0
- package/docs/decisions/0005-official-skill-orchestration-and-local-content-memory.md +97 -0
- package/docs/decisions/0006-separate-wechat-user-login-from-account-authorization.md +87 -0
- package/docs/decisions/0007-explicit-personal-wechat-start.md +67 -0
- package/docs/decisions/0008-end-to-end-content-workflow-continuity.md +115 -0
- package/docs/decisions/0009-privileged-multitenant-draft-scheduling.md +36 -0
- package/docs/decisions/0009-versioned-cloud-template-catalog.md +39 -0
- package/docs/eight-stage-implementation-audit.md +62 -0
- package/docs/first-user-guide.md +187 -0
- package/docs/history/content-forge-prd-v0.2-summary.md +81 -0
- package/docs/local-development.md +511 -0
- package/docs/references/aliyun-oss-production-setup.md +89 -0
- package/docs/references/legacy-content-to-wechat-contract.md +223 -0
- package/docs/references/renderer-compatibility-report.md +68 -0
- package/docs/references/source-inventory.md +179 -0
- package/docs/references/wechat-renderer-platform-validation.md +92 -0
- package/docs/references/wechat-third-party-platform-setup.md +159 -0
- package/docs/references/wechat-website-login-setup.md +137 -0
- package/docs/references/wemd-template-attribution.md +25 -0
- package/docs/research-monitoring-design.md +235 -0
- package/docs/todo-preview-local-first.md +31 -0
- package/docs/workbuddy-first-user-runbook.md +246 -0
- package/docs//345/221/230/345/267/245BCDE/347/232/204skill/employee-b-research-analyst/SKILL.md +230 -0
- package/docs//345/221/230/345/267/245BCDE/347/232/204skill/employee-c-outline-architect/SKILL.md +194 -0
- package/docs//345/221/230/345/267/245BCDE/347/232/204skill/employee-d-content-writer/SKILL.md +296 -0
- package/docs//345/221/230/345/267/245BCDE/347/232/204skill/employee-e-visual-designer/SKILL.md +268 -0
- package/package.json +25 -0
- package/skills/dxc-article-outline/SKILL.md +82 -0
- package/skills/dxc-article-outline/agents/openai.yaml +6 -0
- package/skills/dxc-article-outline/references/outline-methods.md +38 -0
- package/skills/dxc-article-write/SKILL.md +85 -0
- package/skills/dxc-article-write/agents/openai.yaml +6 -0
- package/skills/dxc-article-write/references/writing-methods.md +42 -0
- package/skills/dxc-content-brief/SKILL.md +81 -0
- package/skills/dxc-content-brief/agents/openai.yaml +6 -0
- package/skills/dxc-content-brief/references/brief-method.md +34 -0
- package/skills/dxc-content-review/SKILL.md +84 -0
- package/skills/dxc-content-review/agents/openai.yaml +6 -0
- package/skills/dxc-content-review/references/review-checklist.md +35 -0
- package/skills/dxc-content-workflow/SKILL.md +190 -0
- package/skills/dxc-content-workflow/agents/openai.yaml +6 -0
- package/skills/dxc-content-workflow/references/catalog.json +136 -0
- package/skills/dxc-content-workflow/references/onboarding-questions.md +107 -0
- package/skills/dxc-content-workflow/references/stage-contract.md +70 -0
- package/skills/dxc-research/SKILL.md +110 -0
- package/skills/dxc-research/agents/openai.yaml +6 -0
- package/skills/dxc-research/references/research-method.md +53 -0
- package/skills/dxc-title-write/SKILL.md +112 -0
- package/skills/dxc-title-write/agents/openai.yaml +6 -0
- package/skills/dxc-title-write/references/title-methods.md +26 -0
- package/skills/dxc-visual-plan/SKILL.md +119 -0
- package/skills/dxc-visual-plan/agents/openai.yaml +6 -0
- package/skills/dxc-visual-plan/references/visual-methods.md +35 -0
- package/skills/dxc-wechat-publisher/SKILL.md +157 -0
- package/skills/dxc-wechat-publisher/agents/openai.yaml +6 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
import { createRequire as __dxcCreateRequire } from "node:module"; const require = __dxcCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
SourceCli,
|
|
4
|
+
SourceCliError
|
|
5
|
+
} from "./chunk-I6VZLNRZ.js";
|
|
6
|
+
import {
|
|
7
|
+
CONTENT_MONITOR_SCHEMA_VERSION,
|
|
8
|
+
LocalStateError,
|
|
9
|
+
contentMonitorAddResultSchema,
|
|
10
|
+
contentMonitorListResultSchema,
|
|
11
|
+
contentMonitorRemoveResultSchema,
|
|
12
|
+
contentMonitorRunResultSchema,
|
|
13
|
+
contentMonitorSourceSchema,
|
|
14
|
+
contentMonitorStatusResultSchema,
|
|
15
|
+
ensurePrivateRegularFile,
|
|
16
|
+
sourceFetchKindSchema
|
|
17
|
+
} from "./chunk-XIHX5YAF.js";
|
|
18
|
+
|
|
19
|
+
// apps/cli/src/monitor.ts
|
|
20
|
+
import { createHash, randomUUID as systemRandomUUID } from "node:crypto";
|
|
21
|
+
import { lstat } from "node:fs/promises";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { DatabaseSync } from "node:sqlite";
|
|
24
|
+
var DATABASE_DISPLAY_PATH = "~/.dxc/source-monitor.sqlite";
|
|
25
|
+
var LEASE_DURATION_MILLISECONDS = 2 * 60 * 1e3;
|
|
26
|
+
var MAXIMUM_HEADER_BINDINGS = 12;
|
|
27
|
+
var MAXIMUM_RSS_ITEMS = 100;
|
|
28
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
29
|
+
var MonitorCliError = class extends Error {
|
|
30
|
+
constructor(code, message) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.name = "MonitorCliError";
|
|
34
|
+
}
|
|
35
|
+
code;
|
|
36
|
+
};
|
|
37
|
+
function asRecord(value) {
|
|
38
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
39
|
+
throw new MonitorCliError("DXC_MONITOR_STATE_INVALID", "\u672C\u5730\u76D1\u63A7\u72B6\u6001\u65E0\u6548\u3002");
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function stringColumn(row, name) {
|
|
44
|
+
const value = row[name];
|
|
45
|
+
if (typeof value !== "string") {
|
|
46
|
+
throw new MonitorCliError("DXC_MONITOR_STATE_INVALID", "\u672C\u5730\u76D1\u63A7\u72B6\u6001\u65E0\u6548\u3002");
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function nullableStringColumn(row, name) {
|
|
51
|
+
const value = row[name];
|
|
52
|
+
if (value === null) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (typeof value !== "string") {
|
|
56
|
+
throw new MonitorCliError("DXC_MONITOR_STATE_INVALID", "\u672C\u5730\u76D1\u63A7\u72B6\u6001\u65E0\u6548\u3002");
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
function numberColumn(row, name) {
|
|
61
|
+
const value = row[name];
|
|
62
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
63
|
+
throw new MonitorCliError("DXC_MONITOR_STATE_INVALID", "\u672C\u5730\u76D1\u63A7\u72B6\u6001\u65E0\u6548\u3002");
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function errorCode(error) {
|
|
68
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
|
|
69
|
+
}
|
|
70
|
+
function bounded(value, maximum) {
|
|
71
|
+
return value.trim().replaceAll(/\s+/gu, " ").slice(0, maximum).trim();
|
|
72
|
+
}
|
|
73
|
+
function sha256(value) {
|
|
74
|
+
return createHash("sha256").update(value).digest("hex");
|
|
75
|
+
}
|
|
76
|
+
function validUuid(value) {
|
|
77
|
+
return UUID_PATTERN.test(value);
|
|
78
|
+
}
|
|
79
|
+
function validUrl(value) {
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = new URL(value);
|
|
83
|
+
} catch {
|
|
84
|
+
throw new MonitorCliError("DXC_MONITOR_SOURCE_INVALID", "\u6765\u6E90\u5730\u5740\u5FC5\u987B\u662F\u6709\u6548\u7684 HTTP(S) URL\u3002");
|
|
85
|
+
}
|
|
86
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.username.length > 0 || parsed.password.length > 0 || parsed.hostname.length === 0) {
|
|
87
|
+
throw new MonitorCliError("DXC_MONITOR_SOURCE_INVALID", "\u6765\u6E90\u5730\u5740\u53EA\u80FD\u662F\u65E0\u51ED\u636E\u7684 HTTP(S) URL\u3002");
|
|
88
|
+
}
|
|
89
|
+
return parsed.toString();
|
|
90
|
+
}
|
|
91
|
+
function parseHeaderBindings(values) {
|
|
92
|
+
if (values.length > MAXIMUM_HEADER_BINDINGS) {
|
|
93
|
+
throw new MonitorCliError("DXC_MONITOR_SOURCE_INVALID", "\u6700\u591A\u7ED1\u5B9A 12 \u4E2A API \u8BF7\u6C42\u5934\u3002");
|
|
94
|
+
}
|
|
95
|
+
return values.map((value) => {
|
|
96
|
+
const [header, environment, ...rest] = value.split("=");
|
|
97
|
+
if (header === void 0 || environment === void 0 || rest.length > 0 || !/^[A-Za-z0-9-]+$/u.test(header) || !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(environment)) {
|
|
98
|
+
throw new MonitorCliError(
|
|
99
|
+
"DXC_MONITOR_SOURCE_INVALID",
|
|
100
|
+
"--header-env \u5FC5\u987B\u4F7F\u7528 Header=\u73AF\u5883\u53D8\u91CF\u540D \u7684\u5F62\u5F0F\u3002"
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return { environment, header };
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function sourceRow(value) {
|
|
107
|
+
const row = asRecord(value);
|
|
108
|
+
let headerEnvironments;
|
|
109
|
+
try {
|
|
110
|
+
headerEnvironments = JSON.parse(stringColumn(row, "header_environments"));
|
|
111
|
+
} catch {
|
|
112
|
+
throw new MonitorCliError("DXC_MONITOR_STATE_INVALID", "\u672C\u5730\u76D1\u63A7\u72B6\u6001\u65E0\u6548\u3002");
|
|
113
|
+
}
|
|
114
|
+
const source = contentMonitorSourceSchema.safeParse({
|
|
115
|
+
bearerEnvironment: nullableStringColumn(row, "bearer_environment"),
|
|
116
|
+
createdAt: stringColumn(row, "created_at"),
|
|
117
|
+
enabled: numberColumn(row, "enabled") === 1,
|
|
118
|
+
headerEnvironments,
|
|
119
|
+
id: stringColumn(row, "id"),
|
|
120
|
+
kind: stringColumn(row, "kind"),
|
|
121
|
+
name: stringColumn(row, "name"),
|
|
122
|
+
rssLimit: nullableNumberColumn(row, "rss_limit"),
|
|
123
|
+
url: stringColumn(row, "url")
|
|
124
|
+
});
|
|
125
|
+
if (!source.success) {
|
|
126
|
+
throw new MonitorCliError("DXC_MONITOR_STATE_INVALID", "\u672C\u5730\u76D1\u63A7\u72B6\u6001\u65E0\u6548\u3002");
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
...source.data,
|
|
130
|
+
lastRunAt: nullableStringColumn(row, "last_run_at"),
|
|
131
|
+
lastRunSummary: nullableStringColumn(row, "last_run_summary"),
|
|
132
|
+
latestItemAt: nullableStringColumn(row, "latest_item_at"),
|
|
133
|
+
observedItemCount: numberColumn(row, "observed_item_count")
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function nullableNumberColumn(row, name) {
|
|
137
|
+
const value = row[name];
|
|
138
|
+
if (value === null) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
142
|
+
throw new MonitorCliError("DXC_MONITOR_STATE_INVALID", "\u672C\u5730\u76D1\u63A7\u72B6\u6001\u65E0\u6548\u3002");
|
|
143
|
+
}
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
function monitorSchema(database) {
|
|
147
|
+
const version = numberColumn(
|
|
148
|
+
asRecord(database.prepare("PRAGMA user_version").get()),
|
|
149
|
+
"user_version"
|
|
150
|
+
);
|
|
151
|
+
if (version !== 0 && version !== Number(CONTENT_MONITOR_SCHEMA_VERSION)) {
|
|
152
|
+
throw new MonitorCliError("DXC_MONITOR_SCHEMA_UNSUPPORTED", "\u672C\u5730\u76D1\u63A7\u6570\u636E\u5E93\u7248\u672C\u4E0D\u53D7\u652F\u6301\u3002");
|
|
153
|
+
}
|
|
154
|
+
database.exec(`
|
|
155
|
+
PRAGMA foreign_keys = ON;
|
|
156
|
+
PRAGMA busy_timeout = 5000;
|
|
157
|
+
PRAGMA journal_mode = DELETE;
|
|
158
|
+
PRAGMA synchronous = FULL;
|
|
159
|
+
|
|
160
|
+
CREATE TABLE IF NOT EXISTS monitor_sources (
|
|
161
|
+
id TEXT PRIMARY KEY,
|
|
162
|
+
name TEXT NOT NULL,
|
|
163
|
+
kind TEXT NOT NULL CHECK (kind IN ('rss', 'api', 'html')),
|
|
164
|
+
url TEXT NOT NULL,
|
|
165
|
+
bearer_environment TEXT,
|
|
166
|
+
header_environments TEXT NOT NULL,
|
|
167
|
+
rss_limit INTEGER,
|
|
168
|
+
enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
|
|
169
|
+
created_at TEXT NOT NULL,
|
|
170
|
+
UNIQUE(kind, url)
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
CREATE TABLE IF NOT EXISTS monitor_runs (
|
|
174
|
+
id TEXT PRIMARY KEY,
|
|
175
|
+
started_at TEXT NOT NULL,
|
|
176
|
+
finished_at TEXT,
|
|
177
|
+
summary TEXT
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
CREATE TABLE IF NOT EXISTS monitor_run_sources (
|
|
181
|
+
run_id TEXT NOT NULL REFERENCES monitor_runs(id) ON DELETE CASCADE,
|
|
182
|
+
source_id TEXT NOT NULL,
|
|
183
|
+
source_name TEXT NOT NULL,
|
|
184
|
+
summary TEXT NOT NULL,
|
|
185
|
+
failure_message TEXT,
|
|
186
|
+
monitored_item_count INTEGER NOT NULL CHECK (monitored_item_count >= 0),
|
|
187
|
+
new_item_count INTEGER NOT NULL CHECK (new_item_count >= 0),
|
|
188
|
+
updated_item_count INTEGER NOT NULL CHECK (updated_item_count >= 0),
|
|
189
|
+
unchanged_item_count INTEGER NOT NULL CHECK (unchanged_item_count >= 0)
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
CREATE TABLE IF NOT EXISTS monitor_items (
|
|
193
|
+
source_id TEXT NOT NULL REFERENCES monitor_sources(id) ON DELETE CASCADE,
|
|
194
|
+
identity TEXT NOT NULL,
|
|
195
|
+
title TEXT NOT NULL,
|
|
196
|
+
canonical_url TEXT,
|
|
197
|
+
published_at TEXT,
|
|
198
|
+
content_hash TEXT NOT NULL,
|
|
199
|
+
first_seen_at TEXT NOT NULL,
|
|
200
|
+
last_seen_at TEXT NOT NULL,
|
|
201
|
+
sightings INTEGER NOT NULL CHECK (sightings > 0),
|
|
202
|
+
PRIMARY KEY (source_id, identity)
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
CREATE TABLE IF NOT EXISTS monitor_leases (
|
|
206
|
+
source_id TEXT PRIMARY KEY REFERENCES monitor_sources(id) ON DELETE CASCADE,
|
|
207
|
+
holder TEXT NOT NULL,
|
|
208
|
+
expires_at TEXT NOT NULL
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
CREATE INDEX IF NOT EXISTS monitor_items_source_seen_idx
|
|
212
|
+
ON monitor_items(source_id, last_seen_at DESC);
|
|
213
|
+
CREATE INDEX IF NOT EXISTS monitor_runs_finished_idx
|
|
214
|
+
ON monitor_runs(finished_at DESC);
|
|
215
|
+
|
|
216
|
+
PRAGMA user_version = 1;
|
|
217
|
+
`);
|
|
218
|
+
}
|
|
219
|
+
async function openDatabase(target) {
|
|
220
|
+
try {
|
|
221
|
+
await ensurePrivateRegularFile(target);
|
|
222
|
+
const database = new DatabaseSync(target);
|
|
223
|
+
try {
|
|
224
|
+
monitorSchema(database);
|
|
225
|
+
return database;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
database.close();
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (error instanceof MonitorCliError) {
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
if (error instanceof LocalStateError) {
|
|
235
|
+
throw new MonitorCliError("DXC_MONITOR_STATE_INVALID", "\u672C\u5730\u76D1\u63A7\u72B6\u6001\u65E0\u6CD5\u5B89\u5168\u8BFB\u53D6\u3002");
|
|
236
|
+
}
|
|
237
|
+
throw new MonitorCliError("DXC_MONITOR_IO_FAILED", "\u65E0\u6CD5\u6253\u5F00\u672C\u5730\u76D1\u63A7\u6570\u636E\u5E93\u3002");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async function databaseExists(target) {
|
|
241
|
+
try {
|
|
242
|
+
const metadata = await lstat(target);
|
|
243
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
244
|
+
throw new MonitorCliError("DXC_MONITOR_STATE_INVALID", "\u672C\u5730\u76D1\u63A7\u72B6\u6001\u65E0\u6548\u3002");
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (error instanceof MonitorCliError) {
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
if (errorCode(error) === "ENOENT") {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
throw new MonitorCliError("DXC_MONITOR_IO_FAILED", "\u65E0\u6CD5\u8BFB\u53D6\u672C\u5730\u76D1\u63A7\u72B6\u6001\u3002");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function sourceRows(database, sourceId) {
|
|
258
|
+
const rows = database.prepare(
|
|
259
|
+
`
|
|
260
|
+
SELECT
|
|
261
|
+
source.id, source.name, source.kind, source.url, source.bearer_environment,
|
|
262
|
+
source.header_environments, source.rss_limit, source.enabled, source.created_at,
|
|
263
|
+
latest.finished_at AS last_run_at, latest_source.summary AS last_run_summary,
|
|
264
|
+
(SELECT MAX(item.last_seen_at) FROM monitor_items item WHERE item.source_id = source.id)
|
|
265
|
+
AS latest_item_at,
|
|
266
|
+
(SELECT COUNT(*) FROM monitor_items item WHERE item.source_id = source.id)
|
|
267
|
+
AS observed_item_count
|
|
268
|
+
FROM monitor_sources source
|
|
269
|
+
LEFT JOIN monitor_runs latest ON latest.id = (
|
|
270
|
+
SELECT run.id
|
|
271
|
+
FROM monitor_runs run
|
|
272
|
+
INNER JOIN monitor_run_sources run_source ON run_source.run_id = run.id
|
|
273
|
+
WHERE run_source.source_id = source.id AND run.finished_at IS NOT NULL
|
|
274
|
+
ORDER BY run.finished_at DESC
|
|
275
|
+
LIMIT 1
|
|
276
|
+
)
|
|
277
|
+
LEFT JOIN monitor_run_sources latest_source
|
|
278
|
+
ON latest_source.run_id = latest.id AND latest_source.source_id = source.id
|
|
279
|
+
WHERE (? IS NULL OR source.id = ?)
|
|
280
|
+
ORDER BY source.created_at ASC, source.id ASC
|
|
281
|
+
`
|
|
282
|
+
).all(sourceId ?? null, sourceId ?? null);
|
|
283
|
+
return rows.map(sourceRow);
|
|
284
|
+
}
|
|
285
|
+
function observedItems(data, source) {
|
|
286
|
+
if (data.kind === "rss") {
|
|
287
|
+
return data.items.map((item) => ({
|
|
288
|
+
contentHash: sha256(JSON.stringify(item)),
|
|
289
|
+
identity: item.url ?? item.id,
|
|
290
|
+
publishedAt: item.publishedAt ?? null,
|
|
291
|
+
title: item.title,
|
|
292
|
+
url: item.url ?? null
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
if (data.kind === "html") {
|
|
296
|
+
return [
|
|
297
|
+
{
|
|
298
|
+
contentHash: data.capture.contentHash,
|
|
299
|
+
identity: data.document.canonicalUrl ?? data.capture.finalUrl,
|
|
300
|
+
publishedAt: null,
|
|
301
|
+
title: data.document.title,
|
|
302
|
+
url: data.document.canonicalUrl ?? data.capture.finalUrl
|
|
303
|
+
}
|
|
304
|
+
];
|
|
305
|
+
}
|
|
306
|
+
return [
|
|
307
|
+
{
|
|
308
|
+
contentHash: data.capture.contentHash,
|
|
309
|
+
identity: data.capture.finalUrl,
|
|
310
|
+
publishedAt: null,
|
|
311
|
+
title: source.name,
|
|
312
|
+
url: data.capture.finalUrl
|
|
313
|
+
}
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
function sourceResultSummary(source, counts) {
|
|
317
|
+
return `${source.name}\uFF1A\u53D1\u73B0 ${counts.created} \u6761\u65B0\u5185\u5BB9\uFF0C\u66F4\u65B0 ${counts.updated} \u6761\uFF0C\u5DF2\u6709 ${counts.unchanged} \u6761\u4FDD\u6301\u4E0D\u53D8\u3002`;
|
|
318
|
+
}
|
|
319
|
+
function persistObservedItems(database, source, items, timestamp) {
|
|
320
|
+
let created = 0;
|
|
321
|
+
let unchanged = 0;
|
|
322
|
+
let updated = 0;
|
|
323
|
+
database.exec("BEGIN IMMEDIATE");
|
|
324
|
+
try {
|
|
325
|
+
const select = database.prepare(
|
|
326
|
+
"SELECT content_hash FROM monitor_items WHERE source_id = ? AND identity = ? LIMIT 1"
|
|
327
|
+
);
|
|
328
|
+
const insert = database.prepare(`
|
|
329
|
+
INSERT INTO monitor_items (
|
|
330
|
+
source_id, identity, title, canonical_url, published_at, content_hash,
|
|
331
|
+
first_seen_at, last_seen_at, sightings
|
|
332
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)
|
|
333
|
+
`);
|
|
334
|
+
const update = database.prepare(`
|
|
335
|
+
UPDATE monitor_items
|
|
336
|
+
SET title = ?, canonical_url = ?, published_at = ?, content_hash = ?,
|
|
337
|
+
last_seen_at = ?, sightings = sightings + 1
|
|
338
|
+
WHERE source_id = ? AND identity = ?
|
|
339
|
+
`);
|
|
340
|
+
for (const item of items) {
|
|
341
|
+
const existing = select.get(source.id, item.identity);
|
|
342
|
+
if (existing === void 0) {
|
|
343
|
+
insert.run(
|
|
344
|
+
source.id,
|
|
345
|
+
item.identity,
|
|
346
|
+
item.title,
|
|
347
|
+
item.url,
|
|
348
|
+
item.publishedAt,
|
|
349
|
+
item.contentHash,
|
|
350
|
+
timestamp,
|
|
351
|
+
timestamp
|
|
352
|
+
);
|
|
353
|
+
created += 1;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const existingHash = stringColumn(asRecord(existing), "content_hash");
|
|
357
|
+
update.run(
|
|
358
|
+
item.title,
|
|
359
|
+
item.url,
|
|
360
|
+
item.publishedAt,
|
|
361
|
+
item.contentHash,
|
|
362
|
+
timestamp,
|
|
363
|
+
source.id,
|
|
364
|
+
item.identity
|
|
365
|
+
);
|
|
366
|
+
if (existingHash === item.contentHash) {
|
|
367
|
+
unchanged += 1;
|
|
368
|
+
} else {
|
|
369
|
+
updated += 1;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
database.exec("COMMIT");
|
|
373
|
+
} catch (error) {
|
|
374
|
+
try {
|
|
375
|
+
database.exec("ROLLBACK");
|
|
376
|
+
} catch {
|
|
377
|
+
}
|
|
378
|
+
if (error instanceof MonitorCliError) {
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
throw new MonitorCliError("DXC_MONITOR_IO_FAILED", "\u65E0\u6CD5\u4FDD\u5B58\u672C\u6B21\u76D1\u63A7\u7ED3\u679C\u3002");
|
|
382
|
+
}
|
|
383
|
+
return { created, unchanged, updated };
|
|
384
|
+
}
|
|
385
|
+
function acquireLease(database, sourceId, holder, now) {
|
|
386
|
+
const expiresAt = new Date(now.getTime() + LEASE_DURATION_MILLISECONDS).toISOString();
|
|
387
|
+
const result = database.prepare(
|
|
388
|
+
`
|
|
389
|
+
INSERT INTO monitor_leases (source_id, holder, expires_at)
|
|
390
|
+
VALUES (?, ?, ?)
|
|
391
|
+
ON CONFLICT(source_id) DO UPDATE SET holder = excluded.holder, expires_at = excluded.expires_at
|
|
392
|
+
WHERE monitor_leases.expires_at <= ?
|
|
393
|
+
`
|
|
394
|
+
).run(sourceId, holder, expiresAt, now.toISOString());
|
|
395
|
+
return result.changes > 0;
|
|
396
|
+
}
|
|
397
|
+
function releaseLease(database, sourceId, holder) {
|
|
398
|
+
database.prepare("DELETE FROM monitor_leases WHERE source_id = ? AND holder = ?").run(sourceId, holder);
|
|
399
|
+
}
|
|
400
|
+
var MonitorCli = class {
|
|
401
|
+
#databasePath;
|
|
402
|
+
#now;
|
|
403
|
+
#randomUUID;
|
|
404
|
+
#sourceCli;
|
|
405
|
+
constructor(dependencies) {
|
|
406
|
+
this.#databasePath = dependencies.databasePath ?? path.join(dependencies.homeDirectory, ".dxc", "source-monitor.sqlite");
|
|
407
|
+
this.#now = dependencies.now ?? (() => /* @__PURE__ */ new Date());
|
|
408
|
+
this.#randomUUID = dependencies.randomUUID ?? systemRandomUUID;
|
|
409
|
+
this.#sourceCli = dependencies.sourceCli ?? new SourceCli(dependencies.sourceDependencies);
|
|
410
|
+
}
|
|
411
|
+
async add(options) {
|
|
412
|
+
const kind = sourceFetchKindSchema.safeParse(options.kind);
|
|
413
|
+
const name = bounded(options.name ?? "", 120);
|
|
414
|
+
const bearerEnvironment = options.bearerEnvironment?.trim() || null;
|
|
415
|
+
if (!kind.success || bearerEnvironment !== null && !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(bearerEnvironment) || kind.data !== "api" && (bearerEnvironment !== null || options.headerEnvironments.length > 0) || options.rssLimit !== void 0 && (!Number.isSafeInteger(options.rssLimit) || options.rssLimit < 1 || options.rssLimit > MAXIMUM_RSS_ITEMS) || kind.data !== "rss" && options.rssLimit !== void 0) {
|
|
416
|
+
throw new MonitorCliError("DXC_MONITOR_SOURCE_INVALID", "\u76D1\u63A7\u6765\u6E90\u914D\u7F6E\u65E0\u6548\u3002");
|
|
417
|
+
}
|
|
418
|
+
const url = validUrl(options.url);
|
|
419
|
+
const source = {
|
|
420
|
+
bearerEnvironment,
|
|
421
|
+
createdAt: this.#now().toISOString(),
|
|
422
|
+
enabled: true,
|
|
423
|
+
headerEnvironments: [...parseHeaderBindings(options.headerEnvironments)],
|
|
424
|
+
id: this.#randomUUID(),
|
|
425
|
+
kind: kind.data,
|
|
426
|
+
name: name || new URL(url).hostname,
|
|
427
|
+
rssLimit: kind.data === "rss" ? options.rssLimit ?? 20 : null,
|
|
428
|
+
url
|
|
429
|
+
};
|
|
430
|
+
const database = await openDatabase(this.#databasePath);
|
|
431
|
+
try {
|
|
432
|
+
const duplicate = database.prepare("SELECT id FROM monitor_sources WHERE kind = ? AND url = ? LIMIT 1").get(source.kind, source.url);
|
|
433
|
+
if (duplicate !== void 0) {
|
|
434
|
+
throw new MonitorCliError("DXC_MONITOR_SOURCE_INVALID", "\u8BE5\u6765\u6E90\u5DF2\u7ECF\u5728\u672C\u5730\u76D1\u63A7\u5217\u8868\u4E2D\u3002");
|
|
435
|
+
}
|
|
436
|
+
database.prepare(
|
|
437
|
+
`
|
|
438
|
+
INSERT INTO monitor_sources (
|
|
439
|
+
id, name, kind, url, bearer_environment, header_environments, rss_limit, enabled, created_at
|
|
440
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
441
|
+
`
|
|
442
|
+
).run(
|
|
443
|
+
source.id,
|
|
444
|
+
source.name,
|
|
445
|
+
source.kind,
|
|
446
|
+
source.url,
|
|
447
|
+
source.bearerEnvironment,
|
|
448
|
+
JSON.stringify(source.headerEnvironments),
|
|
449
|
+
source.rssLimit,
|
|
450
|
+
source.enabled ? 1 : 0,
|
|
451
|
+
source.createdAt
|
|
452
|
+
);
|
|
453
|
+
return contentMonitorAddResultSchema.parse({
|
|
454
|
+
command: "monitor.add",
|
|
455
|
+
data: { source, summary: `\u5DF2\u6DFB\u52A0\u201C${source.name}\u201D\uFF1B\u53EF\u8FD0\u884C monitor run \u8FDB\u884C\u9996\u6B21\u68C0\u67E5\u3002` },
|
|
456
|
+
ok: true
|
|
457
|
+
});
|
|
458
|
+
} catch (error) {
|
|
459
|
+
if (error instanceof MonitorCliError) {
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
throw new MonitorCliError("DXC_MONITOR_IO_FAILED", "\u65E0\u6CD5\u4FDD\u5B58\u672C\u5730\u76D1\u63A7\u6765\u6E90\u3002");
|
|
463
|
+
} finally {
|
|
464
|
+
database.close();
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async list() {
|
|
468
|
+
if (!await databaseExists(this.#databasePath)) {
|
|
469
|
+
return contentMonitorListResultSchema.parse({
|
|
470
|
+
command: "monitor.list",
|
|
471
|
+
data: { sources: [], summary: "\u672C\u5730\u5C1A\u672A\u6DFB\u52A0\u76D1\u63A7\u6765\u6E90\u3002" },
|
|
472
|
+
ok: true
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
const database = await openDatabase(this.#databasePath);
|
|
476
|
+
try {
|
|
477
|
+
const sources = sourceRows(database);
|
|
478
|
+
return contentMonitorListResultSchema.parse({
|
|
479
|
+
command: "monitor.list",
|
|
480
|
+
data: {
|
|
481
|
+
sources,
|
|
482
|
+
summary: sources.length === 0 ? "\u672C\u5730\u5C1A\u672A\u6DFB\u52A0\u76D1\u63A7\u6765\u6E90\u3002" : `\u5F53\u524D\u6709 ${sources.length} \u4E2A\u672C\u5730\u76D1\u63A7\u6765\u6E90\u3002`
|
|
483
|
+
},
|
|
484
|
+
ok: true
|
|
485
|
+
});
|
|
486
|
+
} finally {
|
|
487
|
+
database.close();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async remove(sourceId) {
|
|
491
|
+
if (!validUuid(sourceId)) {
|
|
492
|
+
throw new MonitorCliError("DXC_MONITOR_SOURCE_INVALID", "\u76D1\u63A7\u6765\u6E90 ID \u65E0\u6548\u3002");
|
|
493
|
+
}
|
|
494
|
+
if (!await databaseExists(this.#databasePath)) {
|
|
495
|
+
return contentMonitorRemoveResultSchema.parse({
|
|
496
|
+
command: "monitor.remove",
|
|
497
|
+
data: { removed: false, sourceId, summary: "\u672A\u627E\u5230\u8981\u79FB\u9664\u7684\u672C\u5730\u76D1\u63A7\u6765\u6E90\u3002" },
|
|
498
|
+
ok: true
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
const database = await openDatabase(this.#databasePath);
|
|
502
|
+
try {
|
|
503
|
+
database.exec("BEGIN IMMEDIATE");
|
|
504
|
+
try {
|
|
505
|
+
const removed = database.prepare("DELETE FROM monitor_sources WHERE id = ?").run(sourceId).changes > 0;
|
|
506
|
+
database.exec("COMMIT");
|
|
507
|
+
return contentMonitorRemoveResultSchema.parse({
|
|
508
|
+
command: "monitor.remove",
|
|
509
|
+
data: {
|
|
510
|
+
removed,
|
|
511
|
+
sourceId,
|
|
512
|
+
summary: removed ? "\u5DF2\u79FB\u9664\u672C\u5730\u76D1\u63A7\u6765\u6E90\u53CA\u5176\u53BB\u91CD\u8BB0\u5F55\u3002" : "\u672A\u627E\u5230\u8981\u79FB\u9664\u7684\u672C\u5730\u76D1\u63A7\u6765\u6E90\u3002"
|
|
513
|
+
},
|
|
514
|
+
ok: true
|
|
515
|
+
});
|
|
516
|
+
} catch {
|
|
517
|
+
database.exec("ROLLBACK");
|
|
518
|
+
throw new MonitorCliError("DXC_MONITOR_IO_FAILED", "\u65E0\u6CD5\u79FB\u9664\u672C\u5730\u76D1\u63A7\u6765\u6E90\u3002");
|
|
519
|
+
}
|
|
520
|
+
} finally {
|
|
521
|
+
database.close();
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
async run(sourceId) {
|
|
525
|
+
if (sourceId !== void 0 && !validUuid(sourceId)) {
|
|
526
|
+
throw new MonitorCliError("DXC_MONITOR_SOURCE_INVALID", "\u76D1\u63A7\u6765\u6E90 ID \u65E0\u6548\u3002");
|
|
527
|
+
}
|
|
528
|
+
const database = await openDatabase(this.#databasePath);
|
|
529
|
+
const startedAt = this.#now().toISOString();
|
|
530
|
+
const runId = this.#randomUUID();
|
|
531
|
+
try {
|
|
532
|
+
const sources = sourceRows(database, sourceId).filter((source) => source.enabled);
|
|
533
|
+
if (sourceId !== void 0 && sources.length === 0) {
|
|
534
|
+
throw new MonitorCliError("DXC_MONITOR_SOURCE_NOT_FOUND", "\u672A\u627E\u5230\u5DF2\u542F\u7528\u7684\u672C\u5730\u76D1\u63A7\u6765\u6E90\u3002");
|
|
535
|
+
}
|
|
536
|
+
database.prepare("INSERT INTO monitor_runs (id, started_at) VALUES (?, ?)").run(runId, startedAt);
|
|
537
|
+
const results = [];
|
|
538
|
+
for (const source of sources) {
|
|
539
|
+
const holder = this.#randomUUID();
|
|
540
|
+
const now = this.#now();
|
|
541
|
+
if (!acquireLease(database, source.id, holder, now)) {
|
|
542
|
+
results.push({
|
|
543
|
+
failureMessage: "\u53E6\u4E00\u4E2A\u672C\u5730\u8FD0\u884C\u4ECD\u5728\u8FDB\u884C\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002",
|
|
544
|
+
monitoredItemCount: 0,
|
|
545
|
+
newItemCount: 0,
|
|
546
|
+
sourceId: source.id,
|
|
547
|
+
sourceName: source.name,
|
|
548
|
+
summary: `${source.name}\uFF1A\u672C\u6B21\u672A\u6267\u884C\uFF0C\u56E0\u4E3A\u5DF2\u6709\u672C\u5730\u8FD0\u884C\u6B63\u5728\u5904\u7406\u3002`,
|
|
549
|
+
unchangedItemCount: 0,
|
|
550
|
+
updatedItemCount: 0
|
|
551
|
+
});
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
const capture = await this.#capture(source);
|
|
556
|
+
const items = observedItems(capture.data, source);
|
|
557
|
+
const counts = persistObservedItems(database, source, items, this.#now().toISOString());
|
|
558
|
+
results.push({
|
|
559
|
+
failureMessage: null,
|
|
560
|
+
monitoredItemCount: items.length,
|
|
561
|
+
newItemCount: counts.created,
|
|
562
|
+
sourceId: source.id,
|
|
563
|
+
sourceName: source.name,
|
|
564
|
+
summary: sourceResultSummary(source, { monitored: items.length, ...counts }),
|
|
565
|
+
unchangedItemCount: counts.unchanged,
|
|
566
|
+
updatedItemCount: counts.updated
|
|
567
|
+
});
|
|
568
|
+
} catch (error) {
|
|
569
|
+
const message = error instanceof SourceCliError || error instanceof MonitorCliError ? bounded(error.message, 500) : "\u6765\u6E90\u6682\u65F6\u65E0\u6CD5\u8BFB\u53D6\uFF0C\u8BF7\u68C0\u67E5\u5730\u5740\u6216\u7A0D\u540E\u518D\u8BD5\u3002";
|
|
570
|
+
results.push({
|
|
571
|
+
failureMessage: message,
|
|
572
|
+
monitoredItemCount: 0,
|
|
573
|
+
newItemCount: 0,
|
|
574
|
+
sourceId: source.id,
|
|
575
|
+
sourceName: source.name,
|
|
576
|
+
summary: `${source.name}\uFF1A\u672C\u6B21\u672A\u5B8C\u6210\u68C0\u67E5\u3002${message}`,
|
|
577
|
+
unchangedItemCount: 0,
|
|
578
|
+
updatedItemCount: 0
|
|
579
|
+
});
|
|
580
|
+
} finally {
|
|
581
|
+
releaseLease(database, source.id, holder);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
const finishedAt = this.#now().toISOString();
|
|
585
|
+
const failedSourceCount = results.filter((result) => result.failureMessage !== null).length;
|
|
586
|
+
const summary = results.length === 0 ? "\u6CA1\u6709\u5DF2\u542F\u7528\u7684\u672C\u5730\u76D1\u63A7\u6765\u6E90\u3002" : failedSourceCount === 0 ? `\u672C\u6B21\u5DF2\u5B8C\u6210 ${results.length} \u4E2A\u6765\u6E90\u7684\u672C\u5730\u68C0\u67E5\u3002` : `\u672C\u6B21\u5B8C\u6210 ${results.length} \u4E2A\u6765\u6E90\u68C0\u67E5\uFF0C\u5176\u4E2D ${failedSourceCount} \u4E2A\u9700\u8981\u7A0D\u540E\u91CD\u8BD5\u3002`;
|
|
587
|
+
this.#saveRun(database, runId, finishedAt, summary, results);
|
|
588
|
+
return contentMonitorRunResultSchema.parse({
|
|
589
|
+
command: "monitor.run",
|
|
590
|
+
data: { failedSourceCount, finishedAt, results, startedAt, summary },
|
|
591
|
+
ok: true
|
|
592
|
+
});
|
|
593
|
+
} finally {
|
|
594
|
+
database.close();
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
async status() {
|
|
598
|
+
if (!await databaseExists(this.#databasePath)) {
|
|
599
|
+
return contentMonitorStatusResultSchema.parse({
|
|
600
|
+
command: "monitor.status",
|
|
601
|
+
data: {
|
|
602
|
+
databasePath: DATABASE_DISPLAY_PATH,
|
|
603
|
+
enabledSourceCount: 0,
|
|
604
|
+
latestRunAt: null,
|
|
605
|
+
latestRunSummary: null,
|
|
606
|
+
sourceCount: 0,
|
|
607
|
+
summary: "\u672C\u5730\u76D1\u63A7\u5C1A\u672A\u521D\u59CB\u5316\uFF1B\u5148\u6DFB\u52A0\u4E00\u4E2A\u516C\u5F00\u6765\u6E90\u3002"
|
|
608
|
+
},
|
|
609
|
+
ok: true
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
const database = await openDatabase(this.#databasePath);
|
|
613
|
+
try {
|
|
614
|
+
const counts = asRecord(
|
|
615
|
+
database.prepare(
|
|
616
|
+
"SELECT COUNT(*) AS source_count, COALESCE(SUM(enabled), 0) AS enabled_source_count FROM monitor_sources"
|
|
617
|
+
).get()
|
|
618
|
+
);
|
|
619
|
+
const latest = database.prepare(
|
|
620
|
+
"SELECT finished_at, summary FROM monitor_runs WHERE finished_at IS NOT NULL ORDER BY finished_at DESC LIMIT 1"
|
|
621
|
+
).get();
|
|
622
|
+
const sourceCount = numberColumn(counts, "source_count");
|
|
623
|
+
const enabledSourceCount = numberColumn(counts, "enabled_source_count");
|
|
624
|
+
const latestRow = latest === void 0 ? void 0 : asRecord(latest);
|
|
625
|
+
const latestRunAt = latestRow === void 0 ? null : nullableStringColumn(latestRow, "finished_at");
|
|
626
|
+
const latestRunSummary = latestRow === void 0 ? null : nullableStringColumn(latestRow, "summary");
|
|
627
|
+
return contentMonitorStatusResultSchema.parse({
|
|
628
|
+
command: "monitor.status",
|
|
629
|
+
data: {
|
|
630
|
+
databasePath: DATABASE_DISPLAY_PATH,
|
|
631
|
+
enabledSourceCount,
|
|
632
|
+
latestRunAt,
|
|
633
|
+
latestRunSummary,
|
|
634
|
+
sourceCount,
|
|
635
|
+
summary: sourceCount === 0 ? "\u672C\u5730\u5C1A\u672A\u6DFB\u52A0\u76D1\u63A7\u6765\u6E90\u3002" : latestRunAt === null ? `\u5DF2\u6709 ${sourceCount} \u4E2A\u6765\u6E90\uFF0C\u5C1A\u672A\u8FDB\u884C\u9996\u6B21\u68C0\u67E5\u3002` : `\u5DF2\u6709 ${enabledSourceCount} \u4E2A\u5DF2\u542F\u7528\u6765\u6E90\uFF1B${latestRunSummary ?? "\u6700\u8FD1\u68C0\u67E5\u5DF2\u5B8C\u6210\u3002"}`
|
|
636
|
+
},
|
|
637
|
+
ok: true
|
|
638
|
+
});
|
|
639
|
+
} finally {
|
|
640
|
+
database.close();
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
async #capture(source) {
|
|
644
|
+
if (source.kind === "rss") {
|
|
645
|
+
return this.#sourceCli.fetchRss(source.url, source.rssLimit ?? 20);
|
|
646
|
+
}
|
|
647
|
+
if (source.kind === "html") {
|
|
648
|
+
return this.#sourceCli.fetchHtml(source.url);
|
|
649
|
+
}
|
|
650
|
+
return this.#sourceCli.fetchApi({
|
|
651
|
+
...source.bearerEnvironment === null ? {} : { bearerEnvironment: source.bearerEnvironment },
|
|
652
|
+
headerEnvironments: source.headerEnvironments.map(
|
|
653
|
+
(binding) => `${binding.header}=${binding.environment}`
|
|
654
|
+
),
|
|
655
|
+
url: source.url
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
#saveRun(database, runId, finishedAt, summary, results) {
|
|
659
|
+
database.exec("BEGIN IMMEDIATE");
|
|
660
|
+
try {
|
|
661
|
+
const insert = database.prepare(`
|
|
662
|
+
INSERT INTO monitor_run_sources (
|
|
663
|
+
run_id, source_id, source_name, summary, failure_message, monitored_item_count,
|
|
664
|
+
new_item_count, updated_item_count, unchanged_item_count
|
|
665
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
666
|
+
`);
|
|
667
|
+
for (const result of results) {
|
|
668
|
+
insert.run(
|
|
669
|
+
runId,
|
|
670
|
+
result.sourceId,
|
|
671
|
+
result.sourceName,
|
|
672
|
+
result.summary,
|
|
673
|
+
result.failureMessage,
|
|
674
|
+
result.monitoredItemCount,
|
|
675
|
+
result.newItemCount,
|
|
676
|
+
result.updatedItemCount,
|
|
677
|
+
result.unchangedItemCount
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
database.prepare("UPDATE monitor_runs SET finished_at = ?, summary = ? WHERE id = ?").run(finishedAt, summary, runId);
|
|
681
|
+
database.exec("COMMIT");
|
|
682
|
+
} catch {
|
|
683
|
+
try {
|
|
684
|
+
database.exec("ROLLBACK");
|
|
685
|
+
} catch {
|
|
686
|
+
}
|
|
687
|
+
throw new MonitorCliError("DXC_MONITOR_IO_FAILED", "\u65E0\u6CD5\u8BB0\u5F55\u672C\u6B21\u672C\u5730\u76D1\u63A7\u8FD0\u884C\u3002 ");
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
export {
|
|
692
|
+
MonitorCli,
|
|
693
|
+
MonitorCliError
|
|
694
|
+
};
|