@yoooclaw/phone-notifications 1.10.0-beta.21 → 1.10.0-beta.23

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/dist/index.js CHANGED
@@ -272,6 +272,154 @@ var init_credentials = __esm({
272
272
  }
273
273
  });
274
274
 
275
+ // src/recording/transcript-document.ts
276
+ function buildTranscriptDataFilename(recordingId) {
277
+ return `${recordingId}.json`;
278
+ }
279
+ function buildTranscriptDocument(params) {
280
+ const segments = normalizeSegments(params.segments);
281
+ const text = normalizeOptionalText(params.text) ?? joinSegmentsText(segments) ?? "";
282
+ return {
283
+ schemaVersion: TRANSCRIPT_DOCUMENT_SCHEMA_VERSION,
284
+ recordingId: params.recordingId,
285
+ generatedAt: params.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
286
+ source: {
287
+ provider: params.source.provider,
288
+ taskId: normalizeOptionalText(params.source.taskId),
289
+ requestId: normalizeOptionalText(params.source.requestId),
290
+ status: normalizeOptionalText(params.source.status)
291
+ },
292
+ normalized: {
293
+ title: normalizeOptionalText(params.title),
294
+ category: normalizeOptionalText(params.category),
295
+ summary: normalizeOptionalText(params.summary),
296
+ text,
297
+ segments
298
+ },
299
+ raw: params.raw
300
+ };
301
+ }
302
+ function parseTranscriptDocument(value) {
303
+ const parsed = typeof value === "string" ? parseJson(value) : value;
304
+ if (!parsed || typeof parsed !== "object") {
305
+ return void 0;
306
+ }
307
+ const doc = parsed;
308
+ const recordingId = normalizeOptionalText(doc.recordingId);
309
+ const generatedAt = normalizeOptionalText(doc.generatedAt);
310
+ const source = normalizeSource(doc.source);
311
+ const normalized = normalizeDocumentBody(doc.normalized);
312
+ if (!recordingId || !generatedAt || !source || !normalized) {
313
+ return void 0;
314
+ }
315
+ return {
316
+ schemaVersion: typeof doc.schemaVersion === "number" ? doc.schemaVersion : TRANSCRIPT_DOCUMENT_SCHEMA_VERSION,
317
+ recordingId,
318
+ generatedAt,
319
+ source,
320
+ normalized,
321
+ raw: doc.raw
322
+ };
323
+ }
324
+ function extractTranscriptTextFromDocument(doc) {
325
+ if (!doc) return void 0;
326
+ return normalizeOptionalText(doc.normalized.text) ?? joinSegmentsText(doc.normalized.segments);
327
+ }
328
+ function extractTranscriptSummaryFromDocument(doc) {
329
+ if (!doc) return void 0;
330
+ return normalizeOptionalText(doc.normalized.summary);
331
+ }
332
+ function extractTranscriptTitleFromDocument(doc) {
333
+ if (!doc) return void 0;
334
+ return normalizeOptionalText(doc.normalized.title);
335
+ }
336
+ function parseJson(value) {
337
+ try {
338
+ return JSON.parse(value);
339
+ } catch {
340
+ return void 0;
341
+ }
342
+ }
343
+ function normalizeSource(value) {
344
+ if (!value || typeof value !== "object") {
345
+ return void 0;
346
+ }
347
+ const source = value;
348
+ const provider = normalizeOptionalText(source.provider);
349
+ if (!provider) {
350
+ return void 0;
351
+ }
352
+ return {
353
+ provider,
354
+ taskId: normalizeOptionalText(source.taskId),
355
+ requestId: normalizeOptionalText(source.requestId),
356
+ status: normalizeOptionalText(source.status)
357
+ };
358
+ }
359
+ function normalizeDocumentBody(value) {
360
+ if (!value || typeof value !== "object") {
361
+ return void 0;
362
+ }
363
+ const normalized = value;
364
+ const segments = normalizeSegments(normalized.segments);
365
+ const text = normalizeOptionalText(normalized.text) ?? joinSegmentsText(segments);
366
+ if (!text) {
367
+ return void 0;
368
+ }
369
+ return {
370
+ title: normalizeOptionalText(normalized.title),
371
+ category: normalizeOptionalText(normalized.category),
372
+ summary: normalizeOptionalText(normalized.summary),
373
+ text,
374
+ segments
375
+ };
376
+ }
377
+ function normalizeSegments(value) {
378
+ if (!Array.isArray(value)) {
379
+ return [];
380
+ }
381
+ return value.map((item) => normalizeSegment(item)).filter((item) => !!item);
382
+ }
383
+ function normalizeSegment(value) {
384
+ if (!value || typeof value !== "object") {
385
+ return void 0;
386
+ }
387
+ const segment = value;
388
+ const text = normalizeOptionalText(segment.text);
389
+ if (!text) {
390
+ return void 0;
391
+ }
392
+ return {
393
+ text,
394
+ startMs: normalizeOptionalNumber(segment.startMs),
395
+ endMs: normalizeOptionalNumber(segment.endMs),
396
+ speakerId: normalizeOptionalInteger(segment.speakerId)
397
+ };
398
+ }
399
+ function joinSegmentsText(segments) {
400
+ const parts = segments.map((segment) => normalizeOptionalText(segment.text)).filter((segment) => !!segment);
401
+ if (parts.length === 0) {
402
+ return void 0;
403
+ }
404
+ return parts.join("\n\n");
405
+ }
406
+ function normalizeOptionalText(value) {
407
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
408
+ }
409
+ function normalizeOptionalNumber(value) {
410
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
411
+ }
412
+ function normalizeOptionalInteger(value) {
413
+ return Number.isInteger(value) ? Number(value) : void 0;
414
+ }
415
+ var TRANSCRIPT_DOCUMENT_SCHEMA_VERSION;
416
+ var init_transcript_document = __esm({
417
+ "src/recording/transcript-document.ts"() {
418
+ "use strict";
419
+ TRANSCRIPT_DOCUMENT_SCHEMA_VERSION = 1;
420
+ }
421
+ });
422
+
275
423
  // src/recording/whisper-local.ts
276
424
  var whisper_local_exports = {};
277
425
  __export(whisper_local_exports, {
@@ -1090,8 +1238,11 @@ function buildTranscriptMarkdown(result, markers, recordingName, durationSec, cr
1090
1238
  lines.push("");
1091
1239
  markerIdx++;
1092
1240
  }
1093
- lines.push(seg.text.trim());
1094
- lines.push("");
1241
+ const segmentText = formatTranscriptSegmentText(seg);
1242
+ if (segmentText) {
1243
+ lines.push(segmentText);
1244
+ lines.push("");
1245
+ }
1095
1246
  }
1096
1247
  while (markerIdx < sortedMarkers.length) {
1097
1248
  const m = sortedMarkers[markerIdx];
@@ -1133,6 +1284,8 @@ async function runTranscriptionWorkflow(params) {
1133
1284
  durationSec,
1134
1285
  createdAt,
1135
1286
  transcriptsDir,
1287
+ transcriptDataDir,
1288
+ summariesDir,
1136
1289
  recordingId,
1137
1290
  logger
1138
1291
  } = params;
@@ -1142,9 +1295,27 @@ async function runTranscriptionWorkflow(params) {
1142
1295
  if (!result.ok) {
1143
1296
  return { ok: false, error: result.error };
1144
1297
  }
1145
- const title = normalizeOptionalText(result.summary) ? normalizeOptionalText(result.summary) : extractSummary(result.text ?? "");
1146
- const summary = normalizeOptionalText(result.summaryText) ? normalizeOptionalText(result.summaryText) : extractTranscriptSummary(result.text ?? "");
1298
+ const title = normalizeOptionalText2(result.summary) ? normalizeOptionalText2(result.summary) : extractSummary(result.text ?? "");
1299
+ const summary = normalizeOptionalText2(result.summaryText) ? normalizeOptionalText2(result.summaryText) : extractTranscriptSummary(result.text ?? "");
1147
1300
  result.summary = title;
1301
+ const transcriptData = buildTranscriptDocument({
1302
+ recordingId,
1303
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1304
+ source: result.sourceInfo ?? {
1305
+ provider: config.mode === "api" ? "model-proxy" : config.mode
1306
+ },
1307
+ title,
1308
+ category: result.category,
1309
+ summary,
1310
+ text: result.text,
1311
+ segments: (result.segments ?? []).map((segment) => ({
1312
+ text: segment.text,
1313
+ startMs: segment.start_ms,
1314
+ endMs: segment.end_ms,
1315
+ speakerId: segment.speaker_id
1316
+ })),
1317
+ raw: result.rawResponse
1318
+ });
1148
1319
  const markdown = buildTranscriptMarkdown(
1149
1320
  result,
1150
1321
  markers,
@@ -1152,21 +1323,35 @@ async function runTranscriptionWorkflow(params) {
1152
1323
  durationSec,
1153
1324
  createdAt
1154
1325
  );
1326
+ const transcriptDataFilename = buildTranscriptDataFilename(recordingId);
1327
+ const transcriptDataPath = join15(transcriptDataDir, transcriptDataFilename);
1328
+ writeFileSync12(
1329
+ transcriptDataPath,
1330
+ JSON.stringify(transcriptData, null, 2),
1331
+ "utf-8"
1332
+ );
1333
+ logger.info(`[asr] \u8F6C\u5199 JSON \u5DF2\u5199\u5165: ${transcriptDataPath}`);
1155
1334
  const safeSummary = title.replace(/[/\\:*?"<>|]/g, "").trim().slice(0, 20);
1156
1335
  const filename = safeSummary ? `${recordingId}_${safeSummary}.md` : `${recordingId}.md`;
1157
1336
  const filePath = join15(transcriptsDir, filename);
1158
1337
  writeFileSync12(filePath, markdown, "utf-8");
1159
1338
  logger.info(`[asr] \u8F6C\u5199\u6587\u672C\u5DF2\u5199\u5165: ${filePath}`);
1339
+ const summaryFilename = `${recordingId}.md`;
1340
+ const summaryFilePath = join15(summariesDir, summaryFilename);
1341
+ writeFileSync12(summaryFilePath, summary, "utf-8");
1342
+ logger.info(`[asr] \u6458\u8981\u6587\u672C\u5DF2\u5199\u5165: ${summaryFilePath}`);
1160
1343
  return {
1161
1344
  ok: true,
1162
1345
  transcriptFilename: filename,
1163
- transcript: result.text ?? "",
1346
+ transcriptDataFilename,
1347
+ summaryFilename,
1348
+ transcript: transcriptData.normalized.text,
1164
1349
  summary,
1165
1350
  title
1166
1351
  };
1167
1352
  }
1168
1353
  async function transcribeWithModelProxy(audioOssUrl, apiConfig, logger) {
1169
- const normalizedAudioOssUrl = normalizeOptionalText(audioOssUrl);
1354
+ const normalizedAudioOssUrl = normalizeOptionalText2(audioOssUrl);
1170
1355
  if (!normalizedAudioOssUrl) {
1171
1356
  return { ok: false, error: "API \u6A21\u5F0F\u7F3A\u5C11 audioOssUrl\uFF0C\u65E0\u6CD5\u8C03\u7528 model-proxy" };
1172
1357
  }
@@ -1199,8 +1384,8 @@ async function transcribeWithModelProxy(audioOssUrl, apiConfig, logger) {
1199
1384
  `[asr-submit-response] \u63D0\u4EA4\u957F\u5F55\u97F3\u4EFB\u52A1\u54CD\u5E94: ${stringifyForLog(raw) ?? "{}"}`
1200
1385
  );
1201
1386
  const data = unwrapResponse(raw);
1202
- const taskId = normalizeOptionalText(data?.taskId);
1203
- const requestId = normalizeOptionalText(data?.requestId);
1387
+ const taskId = normalizeOptionalText2(data?.taskId);
1388
+ const requestId = normalizeOptionalText2(data?.requestId);
1204
1389
  const status = normalizeLongRecordingStatus(data?.status);
1205
1390
  if (!taskId) {
1206
1391
  return {
@@ -1238,6 +1423,10 @@ async function transcribeWithWhisperLocal2(audioFilePath, config, logger) {
1238
1423
  if (result.ok && result.text) {
1239
1424
  const { extractSummary: extract } = await Promise.resolve().then(() => (init_asr(), asr_exports));
1240
1425
  result.summary = extract(result.text);
1426
+ result.sourceInfo = {
1427
+ provider: "whisper-local",
1428
+ status: "SUCCEEDED"
1429
+ };
1241
1430
  }
1242
1431
  return result;
1243
1432
  }
@@ -1301,7 +1490,7 @@ async function pollLongRecordingTaskResult(params) {
1301
1490
  const raw = await res.json();
1302
1491
  const data = unwrapResponse(raw);
1303
1492
  const status = normalizeLongRecordingStatus(data?.status) ?? "UNKNOWN";
1304
- const requestId = normalizeOptionalText(data?.requestId) ?? initialRequestId;
1493
+ const requestId = normalizeOptionalText2(data?.requestId) ?? initialRequestId;
1305
1494
  if (status !== lastStatus) {
1306
1495
  logger.info(
1307
1496
  `[asr-query-response] \u957F\u5F55\u97F3\u4EFB\u52A1\u67E5\u8BE2\u54CD\u5E94: taskId=${taskId}, attempt=${attempt}, body=${stringifyForLog(raw) ?? "{}"}`
@@ -1336,19 +1525,24 @@ async function pollLongRecordingTaskResult(params) {
1336
1525
  };
1337
1526
  }
1338
1527
  function buildLongRecordingSuccessResult(taskId, requestId, data, logger) {
1339
- const sourceText = normalizeOptionalText(data?.recordResult?.sourceText);
1340
- const summaryText = normalizeOptionalText(data?.recordResult?.summaryResult);
1341
- const title = normalizeOptionalText(data?.recordResult?.title);
1342
- const text = sourceText ?? summaryText;
1528
+ const sourceTextList = normalizeLongRecordingSourceTextList(
1529
+ data?.recordResult?.sourceTextList
1530
+ );
1531
+ const listResult = extractLongRecordingTextFromList(sourceTextList);
1532
+ const sourceText = normalizeOptionalText2(data?.recordResult?.sourceText);
1533
+ const summaryText = normalizeOptionalText2(data?.recordResult?.summaryResult);
1534
+ const title = normalizeOptionalText2(data?.recordResult?.title);
1535
+ const category = normalizeOptionalText2(data?.recordResult?.category);
1536
+ const text = listResult.text ?? sourceText ?? summaryText;
1343
1537
  if (!text) {
1344
1538
  return {
1345
1539
  ok: false,
1346
- error: "Model Proxy ASR \u4EFB\u52A1\u5DF2\u5B8C\u6210\uFF0C\u4F46\u672A\u8FD4\u56DE sourceText \u6216 summaryResult"
1540
+ error: "Model Proxy ASR \u4EFB\u52A1\u5DF2\u5B8C\u6210\uFF0C\u4F46\u672A\u8FD4\u56DE sourceTextList\u3001sourceText \u6216 summaryResult"
1347
1541
  };
1348
1542
  }
1349
- if (!sourceText && summaryText) {
1543
+ if (!listResult.text && !sourceText && summaryText) {
1350
1544
  logger.warn(
1351
- `[asr] Model Proxy \u957F\u5F55\u97F3\u7ED3\u679C\u7F3A\u5C11 sourceText\uFF0C\u5DF2\u56DE\u9000\u4F7F\u7528 summaryResult \u4F5C\u4E3A\u8F6C\u5199\u6587\u672C: taskId=${taskId}`
1545
+ `[asr] Model Proxy \u957F\u5F55\u97F3\u7ED3\u679C\u7F3A\u5C11 sourceTextList/sourceText\uFF0C\u5DF2\u56DE\u9000\u4F7F\u7528 summaryResult \u4F5C\u4E3A\u8F6C\u5199\u6587\u672C: taskId=${taskId}`
1352
1546
  );
1353
1547
  }
1354
1548
  logger.info(
@@ -1357,13 +1551,21 @@ function buildLongRecordingSuccessResult(taskId, requestId, data, logger) {
1357
1551
  return {
1358
1552
  ok: true,
1359
1553
  text,
1360
- segments: [],
1554
+ segments: listResult.segments,
1361
1555
  summary: title ?? extractSummary(text),
1362
- summaryText: summaryText ?? extractTranscriptSummary(text)
1556
+ summaryText: summaryText ?? extractTranscriptSummary(text),
1557
+ category,
1558
+ sourceInfo: {
1559
+ provider: "model-proxy",
1560
+ taskId,
1561
+ requestId,
1562
+ status: "SUCCEEDED"
1563
+ },
1564
+ rawResponse: data
1363
1565
  };
1364
1566
  }
1365
1567
  function buildLongRecordingStatusError(data, status) {
1366
- const errorMessage = normalizeOptionalText(data?.errorMessage);
1568
+ const errorMessage = normalizeOptionalText2(data?.errorMessage);
1367
1569
  return errorMessage ? `Model Proxy ASR ${status}: ${errorMessage}` : `Model Proxy ASR ${status}`;
1368
1570
  }
1369
1571
  function unwrapResponse(payload) {
@@ -1378,9 +1580,56 @@ function normalizeApiKeyHeaderValue(apiKey) {
1378
1580
  function normalizeLongRecordingStatus(status) {
1379
1581
  return typeof status === "string" && status.trim() ? status.trim().toUpperCase() : void 0;
1380
1582
  }
1381
- function normalizeOptionalText(value) {
1583
+ function normalizeOptionalText2(value) {
1382
1584
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
1383
1585
  }
1586
+ function normalizeOptionalInteger2(value) {
1587
+ return Number.isInteger(value) ? Number(value) : void 0;
1588
+ }
1589
+ function normalizeOptionalNonNegativeNumber(value) {
1590
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
1591
+ }
1592
+ function normalizeLongRecordingSourceTextList(value) {
1593
+ if (!Array.isArray(value)) {
1594
+ return [];
1595
+ }
1596
+ return value.map((item) => {
1597
+ if (!item || typeof item !== "object") {
1598
+ return void 0;
1599
+ }
1600
+ const record = item;
1601
+ const content = normalizeOptionalText2(record.content);
1602
+ if (!content) {
1603
+ return void 0;
1604
+ }
1605
+ return {
1606
+ content,
1607
+ speakerId: normalizeOptionalInteger2(record.speakerId),
1608
+ startTime: normalizeOptionalNonNegativeNumber(record.startTime)
1609
+ };
1610
+ }).filter((item) => !!item);
1611
+ }
1612
+ function extractLongRecordingTextFromList(items) {
1613
+ if (items.length === 0) {
1614
+ return {
1615
+ segments: []
1616
+ };
1617
+ }
1618
+ const segments = items.map((item, index) => {
1619
+ const startMs = item.startTime ?? 0;
1620
+ const nextStart = items[index + 1]?.startTime;
1621
+ return {
1622
+ start_ms: startMs,
1623
+ end_ms: nextStart ?? startMs,
1624
+ text: item.content,
1625
+ speaker_id: item.speakerId
1626
+ };
1627
+ });
1628
+ return {
1629
+ text: items.map((item) => item.content).join("\n\n"),
1630
+ segments
1631
+ };
1632
+ }
1384
1633
  function stringifyForLog(value, maxLength = 500) {
1385
1634
  if (value == null) {
1386
1635
  return void 0;
@@ -1426,12 +1675,23 @@ function formatDuration(seconds) {
1426
1675
  function hasText(value) {
1427
1676
  return typeof value === "string" && value.trim().length > 0;
1428
1677
  }
1678
+ function formatTranscriptSegmentText(segment) {
1679
+ const text = segment.text.trim();
1680
+ if (!text) {
1681
+ return text;
1682
+ }
1683
+ if (typeof segment.speaker_id === "number") {
1684
+ return `\u8BF4\u8BDD\u4EBA${segment.speaker_id}\uFF1A${text}`;
1685
+ }
1686
+ return text;
1687
+ }
1429
1688
  var DEFAULT_LONG_RECORDING_POLL_INTERVAL_MS, DEFAULT_LONG_RECORDING_MAX_POLL_ATTEMPTS, LONG_RECORDING_RUNNING_STATUSES, LONG_RECORDING_TERMINAL_FAILURE_STATUSES;
1430
1689
  var init_asr = __esm({
1431
1690
  "src/recording/asr.ts"() {
1432
1691
  "use strict";
1433
1692
  init_credentials();
1434
1693
  init_env();
1694
+ init_transcript_document();
1435
1695
  DEFAULT_LONG_RECORDING_POLL_INTERVAL_MS = 2e3;
1436
1696
  DEFAULT_LONG_RECORDING_MAX_POLL_ATTEMPTS = 150;
1437
1697
  LONG_RECORDING_RUNNING_STATUSES = /* @__PURE__ */ new Set(["PENDING", "RUNNING", "SUSPENDED"]);
@@ -5623,6 +5883,31 @@ if __name__ == "__main__":
5623
5883
  }
5624
5884
 
5625
5885
  // src/light-rules/storage.ts
5886
+ var LEGACY_DEFAULT_CRON_SCHEDULE = "*/5 * * * *";
5887
+ function legacyReadMatchRules(input) {
5888
+ return input.matchRules ?? {};
5889
+ }
5890
+ function legacyReadCronSchedule(input) {
5891
+ return input.cronSchedule ?? LEGACY_DEFAULT_CRON_SCHEDULE;
5892
+ }
5893
+ function legacyHasMatchRules(input) {
5894
+ return input.matchRules !== void 0;
5895
+ }
5896
+ function legacyHasCronSchedule(input) {
5897
+ return input.cronSchedule !== void 0;
5898
+ }
5899
+ function legacyAssignMatchRules(meta, matchRules) {
5900
+ meta.matchRules = matchRules;
5901
+ }
5902
+ function legacyAssignCronSchedule(meta, cronSchedule) {
5903
+ meta.cronSchedule = cronSchedule;
5904
+ }
5905
+ function legacyReadMetaMatchRules(meta) {
5906
+ return meta.matchRules ?? {};
5907
+ }
5908
+ function legacyReadMetaCronSchedule(meta) {
5909
+ return meta.cronSchedule;
5910
+ }
5626
5911
  function resolveBaseDir(ctx) {
5627
5912
  if (ctx.workspaceDir) return ctx.workspaceDir;
5628
5913
  if (ctx.stateDir) {
@@ -5746,21 +6031,23 @@ function createLightRule(ctx, params) {
5746
6031
  repeat_times: params.repeat_times
5747
6032
  });
5748
6033
  assertAncsRepeatTimes(repeatTimes);
6034
+ const effectiveMatchRules = legacyReadMatchRules(params);
6035
+ const effectiveCronSchedule = legacyReadCronSchedule(params);
5749
6036
  const meta = {
5750
6037
  name: params.name,
5751
6038
  type: "light-rule",
5752
6039
  description: params.description,
5753
- matchRules: params.matchRules,
5754
6040
  segments: params.segments,
5755
6041
  repeat_times: repeatTimes,
5756
- cronSchedule: params.cronSchedule,
5757
6042
  enabled: true,
5758
6043
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
5759
6044
  };
6045
+ legacyAssignMatchRules(meta, effectiveMatchRules);
6046
+ legacyAssignCronSchedule(meta, effectiveCronSchedule);
5760
6047
  writeMeta(taskDir, meta);
5761
6048
  writeFileSync(
5762
6049
  join3(taskDir, "fetch.py"),
5763
- generateFetchPy(params.name, params.matchRules),
6050
+ generateFetchPy(params.name, effectiveMatchRules),
5764
6051
  "utf-8"
5765
6052
  );
5766
6053
  writeFileSync(
@@ -5774,7 +6061,7 @@ function createLightRule(ctx, params) {
5774
6061
  action: "add",
5775
6062
  job: {
5776
6063
  name: `notif-${params.name}`,
5777
- schedule: params.cronSchedule,
6064
+ schedule: effectiveCronSchedule,
5778
6065
  sessionTarget: "isolated",
5779
6066
  message: `\u624B\u673A\u901A\u77E5\u5DF2\u7531\u72EC\u7ACB\u670D\u52A1\u5B9E\u65F6\u6355\u83B7\u5230 notifications/ \u76EE\u5F55\u7684 JSON \u6587\u4EF6\u4E2D\u3002
5780
6067
  \u6267\u884C\uFF1Apython3 tasks/${params.name}/fetch.py --notifications-dir notifications
@@ -5797,8 +6084,8 @@ function updateLightRule(ctx, params) {
5797
6084
  meta.description = params.description;
5798
6085
  regenerateReadme = true;
5799
6086
  }
5800
- if (params.matchRules !== void 0) {
5801
- meta.matchRules = params.matchRules;
6087
+ if (legacyHasMatchRules(params)) {
6088
+ legacyAssignMatchRules(meta, legacyReadMatchRules(params));
5802
6089
  regenerateFetch = true;
5803
6090
  }
5804
6091
  if (params.segments !== void 0) {
@@ -5813,8 +6100,8 @@ function updateLightRule(ctx, params) {
5813
6100
  assertAncsRepeatTimes(meta.repeat_times);
5814
6101
  regenerateReadme = true;
5815
6102
  }
5816
- if (params.cronSchedule !== void 0) {
5817
- meta.cronSchedule = params.cronSchedule;
6103
+ if (legacyHasCronSchedule(params)) {
6104
+ legacyAssignCronSchedule(meta, legacyReadCronSchedule(params));
5818
6105
  }
5819
6106
  if (params.enabled !== void 0) {
5820
6107
  meta.enabled = params.enabled;
@@ -5824,7 +6111,7 @@ function updateLightRule(ctx, params) {
5824
6111
  if (regenerateFetch) {
5825
6112
  writeFileSync(
5826
6113
  join3(taskDir, "fetch.py"),
5827
- generateFetchPy(meta.name, meta.matchRules),
6114
+ generateFetchPy(meta.name, legacyReadMetaMatchRules(meta)),
5828
6115
  "utf-8"
5829
6116
  );
5830
6117
  }
@@ -5835,11 +6122,11 @@ function updateLightRule(ctx, params) {
5835
6122
  "utf-8"
5836
6123
  );
5837
6124
  }
5838
- const cronHint = params.cronSchedule !== void 0 ? {
6125
+ const cronHint = legacyHasCronSchedule(params) ? {
5839
6126
  action: "update",
5840
6127
  job: {
5841
6128
  name: `notif-${meta.name}`,
5842
- schedule: meta.cronSchedule
6129
+ schedule: legacyReadMetaCronSchedule(meta)
5843
6130
  }
5844
6131
  } : void 0;
5845
6132
  return { meta, cronHint };
@@ -5881,7 +6168,7 @@ function resolveRuleIdentifier(params) {
5881
6168
  }
5882
6169
  return void 0;
5883
6170
  }
5884
- function registerLightRulesGateway(api, ctx, logger, rememberBroadcast) {
6171
+ function registerLightRulesGateway(api, registry, logger, rememberBroadcast) {
5885
6172
  const registerGatewayMethodWithBroadcastCapture = (method, handler) => {
5886
6173
  api.registerGatewayMethod(method, (opts) => {
5887
6174
  rememberBroadcast?.(opts.context?.broadcast);
@@ -5890,7 +6177,7 @@ function registerLightRulesGateway(api, ctx, logger, rememberBroadcast) {
5890
6177
  };
5891
6178
  registerGatewayMethodWithBroadcastCapture("lightrules.list", async ({ respond }) => {
5892
6179
  try {
5893
- const rules = listLightRules(ctx).map((rule) => ({
6180
+ const rules = registry.list().map((rule) => ({
5894
6181
  ...rule,
5895
6182
  id: rule.name
5896
6183
  }));
@@ -5934,7 +6221,7 @@ function registerLightRulesGateway(api, ctx, logger, rememberBroadcast) {
5934
6221
  return;
5935
6222
  }
5936
6223
  try {
5937
- const result = createLightRule(ctx, {
6224
+ const result = await registry.create({
5938
6225
  name,
5939
6226
  description,
5940
6227
  matchRules,
@@ -5994,7 +6281,7 @@ function registerLightRulesGateway(api, ctx, logger, rememberBroadcast) {
5994
6281
  }
5995
6282
  }
5996
6283
  try {
5997
- const result = updateLightRule(ctx, {
6284
+ const result = await registry.update({
5998
6285
  name,
5999
6286
  description,
6000
6287
  matchRules,
@@ -6031,7 +6318,7 @@ function registerLightRulesGateway(api, ctx, logger, rememberBroadcast) {
6031
6318
  return;
6032
6319
  }
6033
6320
  try {
6034
- const result = deleteLightRule(ctx, name);
6321
+ const result = await registry.delete(name);
6035
6322
  logger.info(`Light rule deleted: ${result.name}`);
6036
6323
  respond(true, {
6037
6324
  ok: true,
@@ -6051,6 +6338,126 @@ function registerLightRulesGateway(api, ctx, logger, rememberBroadcast) {
6051
6338
  });
6052
6339
  }
6053
6340
 
6341
+ // src/light-rules/registry.ts
6342
+ var LightRuleRegistry = class {
6343
+ ctx;
6344
+ /** name → meta 的内存索引;落盘成功后才更新 */
6345
+ index = /* @__PURE__ */ new Map();
6346
+ /** 写路径串行化锁:每次 mutate 都 chain 在前一次之后 */
6347
+ writeChain = Promise.resolve();
6348
+ constructor(ctx) {
6349
+ this.ctx = ctx;
6350
+ this.reload();
6351
+ }
6352
+ /**
6353
+ * 从磁盘重新加载全部规则到内存。仅在构造时和外部显式触发时使用。
6354
+ * 正常 CRUD 路径不应该调用本方法 —— 通过 create/update/delete 增量维护即可。
6355
+ */
6356
+ reload() {
6357
+ this.index.clear();
6358
+ for (const meta of listLightRules(this.ctx)) {
6359
+ this.index.set(meta.name, meta);
6360
+ }
6361
+ }
6362
+ /** 全部规则(包含 disabled),调用方不要改返回值。 */
6363
+ list() {
6364
+ return Array.from(this.index.values());
6365
+ }
6366
+ /** 仅 enabled 的规则。事件驱动评估链路使用。 */
6367
+ getEnabled() {
6368
+ return this.list().filter((rule) => rule.enabled);
6369
+ }
6370
+ /** 按名字精确查找;不存在返回 null。 */
6371
+ get(name) {
6372
+ return this.index.get(name) ?? null;
6373
+ }
6374
+ /**
6375
+ * 创建规则。落盘成功后再写入内存索引。
6376
+ * 失败时 storage 抛 LightRuleError,内存索引保持不变。
6377
+ */
6378
+ async create(params) {
6379
+ return this.runExclusive(() => {
6380
+ const result = createLightRule(this.ctx, params);
6381
+ this.index.set(result.meta.name, result.meta);
6382
+ return result;
6383
+ });
6384
+ }
6385
+ /**
6386
+ * 更新规则。落盘成功后再刷新内存条目。
6387
+ */
6388
+ async update(params) {
6389
+ return this.runExclusive(() => {
6390
+ const result = updateLightRule(this.ctx, params);
6391
+ this.index.set(result.meta.name, result.meta);
6392
+ return result;
6393
+ });
6394
+ }
6395
+ /**
6396
+ * 删除规则。落盘成功后再从内存索引移除。
6397
+ */
6398
+ async delete(name) {
6399
+ return this.runExclusive(() => {
6400
+ const result = deleteLightRule(this.ctx, name);
6401
+ this.index.delete(result.name);
6402
+ return result;
6403
+ });
6404
+ }
6405
+ /**
6406
+ * 拼装 Agent 评估用的 system prompt。
6407
+ *
6408
+ * 当前实现是一个最小可用骨架:列出全部 enabled 规则的 name + description。
6409
+ * M3 接入真实 Agent 链路时,会按 prd-lightrules-event-driven-refactor.md §Prompt 拼装
6410
+ * 的模板补齐"任务说明 / 输出约束"等段落。
6411
+ *
6412
+ * 现在先把方法签名冻结,避免后续散落在多个调用点。
6413
+ */
6414
+ buildSystemPrompt() {
6415
+ const enabled = this.getEnabled();
6416
+ if (enabled.length === 0) {
6417
+ return "\u5F53\u524D\u6CA1\u6709\u542F\u7528\u4EFB\u4F55\u706F\u6548\u89C4\u5219\u3002";
6418
+ }
6419
+ const lines = [
6420
+ '\u4F60\u662F\u4E00\u4E2A\u5224\u65AD"\u901A\u77E5 \u2192 \u706F\u6548\u89C4\u5219"\u662F\u5426\u547D\u4E2D\u7684\u52A9\u624B\u3002',
6421
+ "",
6422
+ "\u4E0B\u9762\u662F\u7528\u6237\u5F53\u524D\u542F\u7528\u7684\u5168\u90E8\u706F\u6548\u89C4\u5219\uFF08\u6309\u521B\u5EFA\u65F6\u95F4\u5012\u5E8F\uFF09\uFF1A",
6423
+ ""
6424
+ ];
6425
+ const sorted = [...enabled].sort(
6426
+ (a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? "")
6427
+ );
6428
+ sorted.forEach((rule, idx) => {
6429
+ lines.push(
6430
+ `[${idx + 1}] name: ${rule.name}`,
6431
+ ` description: ${rule.description}`,
6432
+ ""
6433
+ );
6434
+ });
6435
+ lines.push(
6436
+ '\u4EFB\u52A1\uFF1A\u6839\u636E\u7528\u6237\u63A5\u4E0B\u6765\u53D1\u7ED9\u4F60\u7684"\u901A\u77E5"\u5185\u5BB9\uFF0C\u5224\u65AD\u54EA\u4E9B\u89C4\u5219\u88AB\u547D\u4E2D\u3002',
6437
+ "- \u547D\u4E2D 0 \u6761\uFF1A\u4E0D\u8C03\u7528\u4EFB\u4F55\u5DE5\u5177\uFF0C\u76F4\u63A5\u7ED3\u675F\u3002",
6438
+ "- \u547D\u4E2D 1 \u6761\u6216\u591A\u6761\uFF1A\u5BF9\u6BCF\u4E00\u6761\u547D\u4E2D\u7684\u89C4\u5219\u8C03\u7528\u4E00\u6B21 trigger_light(rule_name)\u3002",
6439
+ "- \u4E0D\u8981\u56DE\u590D\u4EFB\u4F55\u81EA\u7531\u6587\u672C\uFF1B\u5224\u5B9A\u7ED3\u679C**\u53EA**\u901A\u8FC7 tool calling \u8868\u8FBE\u3002",
6440
+ '- \u62FF\u4E0D\u51C6\u7684\u65F6\u5019\u503E\u5411\u4E8E"\u4E0D\u89E6\u53D1"\uFF0C\u907F\u514D\u9A9A\u6270\u7528\u6237\u3002'
6441
+ );
6442
+ return lines.join("\n");
6443
+ }
6444
+ /**
6445
+ * 把一段同步操作排队进 write chain,保证 mutate 串行执行。
6446
+ *
6447
+ * 用 promise 链做 mutex 是最简单的方案:每次 runExclusive 都把
6448
+ * `writeChain` 推进一步。即使前一个任务失败,链也会继续往下走,
6449
+ * 后续任务不会被永久阻塞。
6450
+ */
6451
+ runExclusive(fn) {
6452
+ const next = this.writeChain.then(
6453
+ () => fn(),
6454
+ () => fn()
6455
+ );
6456
+ this.writeChain = next.catch(() => void 0);
6457
+ return next;
6458
+ }
6459
+ };
6460
+
6054
6461
  // src/plugin/auto-update.ts
6055
6462
  init_env();
6056
6463
 
@@ -7761,7 +8168,10 @@ function registerRecStatus(rec, ctx) {
7761
8168
  markers: entry.metadata.markers ?? [],
7762
8169
  audioFile: entry.audioFile ?? null,
7763
8170
  srtFile: entry.srtFile ?? null,
8171
+ transcriptDataFile: entry.transcriptDataFile ?? null,
7764
8172
  transcriptFile: entry.transcriptFile ?? null,
8173
+ summaryFile: entry.summaryFile ?? null,
8174
+ title: entry.title ?? null,
7765
8175
  ingestedAt: entry.ingestedAt,
7766
8176
  updatedAt: entry.updatedAt
7767
8177
  }
@@ -8374,11 +8784,26 @@ var NotificationStorage = class {
8374
8784
  ingested: 0,
8375
8785
  dedupedById: 0,
8376
8786
  dedupedByContent: 0,
8377
- invalid: 0
8787
+ invalid: 0,
8788
+ inserted: []
8378
8789
  };
8379
8790
  for (const n of items) {
8380
8791
  const outcome = await this.writeNotification(n);
8381
- result[outcome] += 1;
8792
+ switch (outcome.kind) {
8793
+ case "ingested":
8794
+ result.ingested += 1;
8795
+ result.inserted.push(outcome.entry);
8796
+ break;
8797
+ case "dedupedById":
8798
+ result.dedupedById += 1;
8799
+ break;
8800
+ case "dedupedByContent":
8801
+ result.dedupedByContent += 1;
8802
+ break;
8803
+ case "invalid":
8804
+ result.invalid += 1;
8805
+ break;
8806
+ }
8382
8807
  }
8383
8808
  this.prune();
8384
8809
  return result;
@@ -8387,7 +8812,7 @@ var NotificationStorage = class {
8387
8812
  const ts = new Date(n.timestamp);
8388
8813
  if (Number.isNaN(ts.getTime())) {
8389
8814
  this.logger.warn(`\u5FFD\u7565\u975E\u6CD5 timestamp \u7684\u901A\u77E5: ${n.id}`);
8390
- return "invalid";
8815
+ return { kind: "invalid" };
8391
8816
  }
8392
8817
  const dateKey = this.formatDate(ts);
8393
8818
  const filePath = join12(this.dir, `${dateKey}.json`);
@@ -8395,10 +8820,10 @@ var NotificationStorage = class {
8395
8820
  const entry = this.buildStoredNotification(n);
8396
8821
  return this.withDateWriteLock(dateKey, async () => {
8397
8822
  if (normalizedId && this.hasNotificationId(dateKey, normalizedId)) {
8398
- return "dedupedById";
8823
+ return { kind: "dedupedById" };
8399
8824
  }
8400
8825
  if (this.hasNotificationContentKey(dateKey, filePath, entry)) {
8401
- return "dedupedByContent";
8826
+ return { kind: "dedupedByContent" };
8402
8827
  }
8403
8828
  const appDisplayName = this.resolveDisplayName ? await this.resolveDisplayName(entry.appName) : entry.appName;
8404
8829
  const storedEntry = {
@@ -8412,7 +8837,7 @@ var NotificationStorage = class {
8412
8837
  this.recordNotificationId(dateKey, normalizedId);
8413
8838
  }
8414
8839
  this.recordNotificationContentKey(dateKey, filePath, storedEntry);
8415
- return "ingested";
8840
+ return { kind: "ingested", entry: storedEntry };
8416
8841
  });
8417
8842
  }
8418
8843
  buildStoredNotification(n) {
@@ -8616,7 +9041,7 @@ import {
8616
9041
  writeFileSync as writeFileSync11,
8617
9042
  rmSync as rmSync5
8618
9043
  } from "fs";
8619
- import { join as join13 } from "path";
9044
+ import { join as join13, basename as basename3 } from "path";
8620
9045
 
8621
9046
  // src/recording/state-machine.ts
8622
9047
  var VALID_TRANSITIONS = /* @__PURE__ */ new Map([
@@ -8654,6 +9079,7 @@ var TransitionError = class extends Error {
8654
9079
  };
8655
9080
 
8656
9081
  // src/recording/storage.ts
9082
+ init_transcript_document();
8657
9083
  function extractAudioExt(ossUrl) {
8658
9084
  try {
8659
9085
  const pathname = new URL(ossUrl).pathname;
@@ -8667,8 +9093,42 @@ function extractAudioExt(ossUrl) {
8667
9093
  }
8668
9094
  var RECORDINGS_DIR = "recordings";
8669
9095
  var AUDIO_DIR = "audio";
9096
+ var TRANSCRIPT_DATA_DIR = "transcript-data";
8670
9097
  var TRANSCRIPTS_DIR = "transcripts";
9098
+ var SUMMARIES_DIR = "summaries";
8671
9099
  var INDEX_FILE = "index.json";
9100
+ function stripMarkdownFence(markdown) {
9101
+ return markdown.replace(/\r\n/g, "\n");
9102
+ }
9103
+ function deriveTitleFromTranscriptPath(transcriptFile, recordingId) {
9104
+ if (!transcriptFile) return void 0;
9105
+ const name = basename3(transcriptFile, ".md");
9106
+ const prefix = `${recordingId}_`;
9107
+ if (name.startsWith(prefix)) {
9108
+ const derived = name.slice(prefix.length).trim();
9109
+ return derived || void 0;
9110
+ }
9111
+ return void 0;
9112
+ }
9113
+ function extractTranscriptContent(markdown) {
9114
+ const normalized = stripMarkdownFence(markdown);
9115
+ const firstDivider = normalized.indexOf("\n---\n");
9116
+ let body = firstDivider >= 0 ? normalized.slice(firstDivider + "\n---\n".length) : normalized;
9117
+ if (body.startsWith("\n")) {
9118
+ body = body.slice(1);
9119
+ }
9120
+ if (body.startsWith("### \u5173\u952E\u70B9")) {
9121
+ const secondDivider = body.indexOf("\n---\n");
9122
+ if (secondDivider >= 0) {
9123
+ body = body.slice(secondDivider + "\n---\n".length);
9124
+ if (body.startsWith("\n")) {
9125
+ body = body.slice(1);
9126
+ }
9127
+ }
9128
+ }
9129
+ const lines = body.split("\n").filter((line) => !/^\*\*\[关键点 .+\]\*\*$/.test(line.trim())).filter((line) => !/^- \*\*\[关键点 .+\]\*\*$/.test(line.trim()));
9130
+ return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
9131
+ }
8672
9132
  function resolveRecordingStorageDir(ctx, logger) {
8673
9133
  const stateRecDir = join13(
8674
9134
  ctx.stateDir,
@@ -8698,18 +9158,24 @@ var RecordingStorage = class {
8698
9158
  this.logger = logger;
8699
9159
  this.dir = dir;
8700
9160
  this.audioDir = join13(dir, AUDIO_DIR);
9161
+ this.transcriptDataDir = join13(dir, TRANSCRIPT_DATA_DIR);
8701
9162
  this.transcriptsDir = join13(dir, TRANSCRIPTS_DIR);
9163
+ this.summariesDir = join13(dir, SUMMARIES_DIR);
8702
9164
  this.indexPath = join13(dir, INDEX_FILE);
8703
9165
  }
8704
9166
  dir;
8705
9167
  audioDir;
9168
+ transcriptDataDir;
8706
9169
  transcriptsDir;
9170
+ summariesDir;
8707
9171
  indexPath;
8708
9172
  index = { recordings: [] };
8709
9173
  /** 初始化目录结构并加载索引 */
8710
9174
  async init() {
8711
9175
  mkdirSync10(this.audioDir, { recursive: true });
9176
+ mkdirSync10(this.transcriptDataDir, { recursive: true });
8712
9177
  mkdirSync10(this.transcriptsDir, { recursive: true });
9178
+ mkdirSync10(this.summariesDir, { recursive: true });
8713
9179
  this.loadIndex();
8714
9180
  this.logger.info(
8715
9181
  `\u5F55\u97F3\u5B58\u50A8\u5DF2\u521D\u59CB\u5316: ${this.dir}\uFF08\u5171 ${this.index.recordings.length} \u6761\u8BB0\u5F55\uFF09`
@@ -8719,10 +9185,18 @@ var RecordingStorage = class {
8719
9185
  getAudioDir() {
8720
9186
  return this.audioDir;
8721
9187
  }
9188
+ /** 获取转写 JSON 目录路径 */
9189
+ getTranscriptDataDir() {
9190
+ return this.transcriptDataDir;
9191
+ }
8722
9192
  /** 获取转写目录路径 */
8723
9193
  getTranscriptsDir() {
8724
9194
  return this.transcriptsDir;
8725
9195
  }
9196
+ /** 获取摘要目录路径 */
9197
+ getSummariesDir() {
9198
+ return this.summariesDir;
9199
+ }
8726
9200
  // ─── 录音入库 ───
8727
9201
  /**
8728
9202
  * 收到 recordings.sync 后,将元数据写入索引。
@@ -8733,9 +9207,21 @@ var RecordingStorage = class {
8733
9207
  const id = recordingId;
8734
9208
  const existing = this.findById(id);
8735
9209
  if (existing) {
9210
+ if (existing.transcriptDataFile) {
9211
+ rmSync5(join13(this.dir, existing.transcriptDataFile), { force: true });
9212
+ }
9213
+ if (existing.transcriptFile) {
9214
+ rmSync5(join13(this.dir, existing.transcriptFile), { force: true });
9215
+ }
9216
+ if (existing.summaryFile) {
9217
+ rmSync5(join13(this.dir, existing.summaryFile), { force: true });
9218
+ }
8736
9219
  existing.metadata = metadata;
8737
9220
  existing.status = "syncing_openclaw";
9221
+ existing.transcriptDataFile = void 0;
8738
9222
  existing.transcriptFile = void 0;
9223
+ existing.summaryFile = void 0;
9224
+ existing.title = void 0;
8739
9225
  existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
8740
9226
  this.logger.info(`\u5F55\u97F3\u5143\u6570\u636E\u5DF2\u66F4\u65B0: ${id}`);
8741
9227
  } else {
@@ -8788,6 +9274,20 @@ var RecordingStorage = class {
8788
9274
  entry.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
8789
9275
  this.saveIndex();
8790
9276
  }
9277
+ /**
9278
+ * 记录转写 JSON 文件路径
9279
+ */
9280
+ setTranscriptDataFile(recordingId, filename) {
9281
+ const entry = this.findById(recordingId);
9282
+ if (!entry) return;
9283
+ const nextTranscriptDataFile = `${TRANSCRIPT_DATA_DIR}/${filename}`;
9284
+ if (entry.transcriptDataFile && entry.transcriptDataFile !== nextTranscriptDataFile) {
9285
+ rmSync5(join13(this.dir, entry.transcriptDataFile), { force: true });
9286
+ }
9287
+ entry.transcriptDataFile = nextTranscriptDataFile;
9288
+ entry.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9289
+ this.saveIndex();
9290
+ }
8791
9291
  /**
8792
9292
  * 记录转写文件路径
8793
9293
  */
@@ -8802,6 +9302,64 @@ var RecordingStorage = class {
8802
9302
  entry.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
8803
9303
  this.saveIndex();
8804
9304
  }
9305
+ /**
9306
+ * 记录摘要文件路径
9307
+ */
9308
+ setSummaryFile(recordingId, filename) {
9309
+ const entry = this.findById(recordingId);
9310
+ if (!entry) return;
9311
+ const nextSummaryFile = `${SUMMARIES_DIR}/${filename}`;
9312
+ if (entry.summaryFile && entry.summaryFile !== nextSummaryFile) {
9313
+ rmSync5(join13(this.dir, entry.summaryFile), { force: true });
9314
+ }
9315
+ entry.summaryFile = nextSummaryFile;
9316
+ entry.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9317
+ this.saveIndex();
9318
+ }
9319
+ /**
9320
+ * 记录转写标题
9321
+ */
9322
+ setTitle(recordingId, title) {
9323
+ const entry = this.findById(recordingId);
9324
+ if (!entry) return;
9325
+ entry.title = title?.trim() || void 0;
9326
+ entry.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9327
+ this.saveIndex();
9328
+ }
9329
+ /**
9330
+ * 读取摘要文本
9331
+ */
9332
+ readSummary(recordingId) {
9333
+ const entry = this.findById(recordingId);
9334
+ if (!entry) return void 0;
9335
+ if (entry.summaryFile) {
9336
+ const summary = this.readRelativeTextFile(entry.summaryFile);
9337
+ if (summary?.trim()) {
9338
+ return summary.trim();
9339
+ }
9340
+ }
9341
+ const transcriptDoc = entry.transcriptDataFile ? this.readRelativeTranscriptDocument(entry.transcriptDataFile) : void 0;
9342
+ return extractTranscriptSummaryFromDocument(transcriptDoc);
9343
+ }
9344
+ /**
9345
+ * 优先从 transcript JSON 中读取正文,旧数据回退读取 Markdown。
9346
+ */
9347
+ readTranscript(recordingId) {
9348
+ const entry = this.findById(recordingId);
9349
+ if (!entry) return void 0;
9350
+ if (entry.transcriptDataFile) {
9351
+ const transcriptDoc = this.readRelativeTranscriptDocument(entry.transcriptDataFile);
9352
+ const transcriptFromJson = extractTranscriptTextFromDocument(transcriptDoc);
9353
+ if (transcriptFromJson) {
9354
+ return transcriptFromJson;
9355
+ }
9356
+ }
9357
+ if (!entry.transcriptFile) return void 0;
9358
+ const markdown = this.readRelativeTextFile(entry.transcriptFile);
9359
+ if (!markdown) return void 0;
9360
+ const transcript = extractTranscriptContent(markdown);
9361
+ return transcript || void 0;
9362
+ }
8805
9363
  // ─── 查询 ───
8806
9364
  findById(id) {
8807
9365
  return this.index.recordings.find((r) => r.id === id);
@@ -8847,14 +9405,24 @@ var RecordingStorage = class {
8847
9405
  const srtPath = join13(this.dir, entry.srtFile);
8848
9406
  rmSync5(srtPath, { force: true });
8849
9407
  }
9408
+ if (entry.transcriptDataFile) {
9409
+ const transcriptDataPath = join13(this.dir, entry.transcriptDataFile);
9410
+ rmSync5(transcriptDataPath, { force: true });
9411
+ }
8850
9412
  if (entry.transcriptFile) {
8851
9413
  const transcriptPath = join13(this.dir, entry.transcriptFile);
8852
9414
  rmSync5(transcriptPath, { force: true });
8853
9415
  }
9416
+ if (entry.summaryFile) {
9417
+ const summaryPath = join13(this.dir, entry.summaryFile);
9418
+ rmSync5(summaryPath, { force: true });
9419
+ }
8854
9420
  if (opts?.localOnly) {
8855
9421
  entry.audioFile = void 0;
8856
9422
  entry.srtFile = void 0;
9423
+ entry.transcriptDataFile = void 0;
8857
9424
  entry.transcriptFile = void 0;
9425
+ entry.summaryFile = void 0;
8858
9426
  entry.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
8859
9427
  this.saveIndex();
8860
9428
  this.logger.info(`\u5F55\u97F3\u672C\u5730\u6587\u4EF6\u5DF2\u5220\u9664\uFF08\u4FDD\u7559\u7D22\u5F15\uFF09: ${recordingId}`);
@@ -8881,6 +9449,12 @@ var RecordingStorage = class {
8881
9449
  buildSrtFilename(recordingId) {
8882
9450
  return `${recordingId}.srt`;
8883
9451
  }
9452
+ /**
9453
+ * 生成转写 JSON 文件名
9454
+ */
9455
+ buildTranscriptDataFilename(recordingId) {
9456
+ return buildTranscriptDataFilename(recordingId);
9457
+ }
8884
9458
  /**
8885
9459
  * 生成转写文件名
8886
9460
  * @param summary 摘要标题(≤ 10 字)
@@ -8889,6 +9463,12 @@ var RecordingStorage = class {
8889
9463
  const safeSummary = summary.replace(/[/\\:*?"<>|]/g, "").trim().slice(0, 20);
8890
9464
  return safeSummary ? `${recordingId}_${safeSummary}.md` : `${recordingId}.md`;
8891
9465
  }
9466
+ /**
9467
+ * 生成摘要文件名
9468
+ */
9469
+ buildSummaryFilename(recordingId) {
9470
+ return `${recordingId}.md`;
9471
+ }
8892
9472
  /**
8893
9473
  * 获取音频文件的绝对路径。ossUrl 用于推断文件扩展名
8894
9474
  */
@@ -8901,6 +9481,18 @@ var RecordingStorage = class {
8901
9481
  getSrtFilePath(recordingId) {
8902
9482
  return join13(this.audioDir, this.buildSrtFilename(recordingId));
8903
9483
  }
9484
+ /**
9485
+ * 获取转写 JSON 文件的绝对路径
9486
+ */
9487
+ getTranscriptDataFilePath(recordingId) {
9488
+ return join13(this.transcriptDataDir, this.buildTranscriptDataFilename(recordingId));
9489
+ }
9490
+ /**
9491
+ * 获取摘要文件的绝对路径
9492
+ */
9493
+ getSummaryFilePath(recordingId) {
9494
+ return join13(this.summariesDir, this.buildSummaryFilename(recordingId));
9495
+ }
8904
9496
  // ─── Persistence ───
8905
9497
  loadIndex() {
8906
9498
  if (!existsSync17(this.indexPath)) {
@@ -8910,6 +9502,7 @@ var RecordingStorage = class {
8910
9502
  try {
8911
9503
  const raw = JSON.parse(readFileSync18(this.indexPath, "utf-8"));
8912
9504
  if (raw && Array.isArray(raw.recordings)) {
9505
+ let needsRewrite = false;
8913
9506
  const normalized = raw.recordings.filter((entry) => entry && typeof entry === "object").map((entry) => {
8914
9507
  const compacted = {
8915
9508
  id: entry.id,
@@ -8923,13 +9516,81 @@ var RecordingStorage = class {
8923
9516
  if (typeof entry.transcriptFile === "string") {
8924
9517
  compacted.transcriptFile = entry.transcriptFile;
8925
9518
  }
9519
+ let transcriptDoc = typeof entry.transcriptDataFile === "string" ? this.readRelativeTranscriptDocument(entry.transcriptDataFile) : void 0;
9520
+ if (typeof entry.transcriptDataFile === "string" && transcriptDoc) {
9521
+ compacted.transcriptDataFile = entry.transcriptDataFile;
9522
+ } else {
9523
+ const legacyTranscriptText = typeof entry.transcript === "string" && entry.transcript.trim() ? entry.transcript.trim() : compacted.transcriptFile ? extractTranscriptContent(
9524
+ this.readRelativeTextFile(compacted.transcriptFile) ?? ""
9525
+ ) || void 0 : void 0;
9526
+ const legacyTitle = typeof entry.title === "string" && entry.title.trim() ? entry.title.trim() : deriveTitleFromTranscriptPath(compacted.transcriptFile, compacted.id);
9527
+ const legacySummary = typeof entry.summary === "string" && entry.summary.trim() ? entry.summary.trim() : void 0;
9528
+ if (legacyTranscriptText) {
9529
+ transcriptDoc = buildTranscriptDocument({
9530
+ recordingId: compacted.id,
9531
+ generatedAt: compacted.updatedAt,
9532
+ source: {
9533
+ provider: "legacy-markdown"
9534
+ },
9535
+ title: legacyTitle,
9536
+ summary: legacySummary,
9537
+ text: legacyTranscriptText,
9538
+ segments: []
9539
+ });
9540
+ const transcriptDataFilename = this.buildTranscriptDataFilename(compacted.id);
9541
+ writeFileSync11(
9542
+ join13(this.transcriptDataDir, transcriptDataFilename),
9543
+ JSON.stringify(transcriptDoc, null, 2),
9544
+ "utf-8"
9545
+ );
9546
+ compacted.transcriptDataFile = `${TRANSCRIPT_DATA_DIR}/${transcriptDataFilename}`;
9547
+ needsRewrite = true;
9548
+ }
9549
+ }
9550
+ if (typeof entry.summaryFile === "string") {
9551
+ compacted.summaryFile = entry.summaryFile;
9552
+ } else if (typeof entry.summary === "string" && entry.summary.trim()) {
9553
+ const summaryFilename = this.buildSummaryFilename(entry.id);
9554
+ writeFileSync11(
9555
+ join13(this.summariesDir, summaryFilename),
9556
+ entry.summary.trim(),
9557
+ "utf-8"
9558
+ );
9559
+ compacted.summaryFile = `${SUMMARIES_DIR}/${summaryFilename}`;
9560
+ needsRewrite = true;
9561
+ } else {
9562
+ const summaryFromDocument = extractTranscriptSummaryFromDocument(transcriptDoc);
9563
+ if (summaryFromDocument) {
9564
+ const summaryFilename = this.buildSummaryFilename(entry.id);
9565
+ writeFileSync11(
9566
+ join13(this.summariesDir, summaryFilename),
9567
+ summaryFromDocument,
9568
+ "utf-8"
9569
+ );
9570
+ compacted.summaryFile = `${SUMMARIES_DIR}/${summaryFilename}`;
9571
+ needsRewrite = true;
9572
+ }
9573
+ }
9574
+ if (typeof entry.title === "string" && entry.title.trim()) {
9575
+ compacted.title = entry.title.trim();
9576
+ } else {
9577
+ const titleFromDocument = extractTranscriptTitleFromDocument(transcriptDoc);
9578
+ const derivedTitle = titleFromDocument ?? deriveTitleFromTranscriptPath(
9579
+ compacted.transcriptFile,
9580
+ compacted.id
9581
+ );
9582
+ if (derivedTitle) {
9583
+ compacted.title = derivedTitle;
9584
+ needsRewrite = true;
9585
+ }
9586
+ }
8926
9587
  return compacted;
8927
9588
  });
8928
9589
  const hadLargeFields = raw.recordings.some(
8929
9590
  (entry) => entry && typeof entry === "object" && ("transcript" in entry || "summary" in entry)
8930
9591
  );
8931
9592
  this.index = { recordings: normalized };
8932
- if (hadLargeFields) {
9593
+ if (hadLargeFields || needsRewrite) {
8933
9594
  this.saveIndex();
8934
9595
  }
8935
9596
  } else {
@@ -8940,6 +9601,20 @@ var RecordingStorage = class {
8940
9601
  this.index = { recordings: [] };
8941
9602
  }
8942
9603
  }
9604
+ readRelativeTextFile(relativePath) {
9605
+ try {
9606
+ return readFileSync18(join13(this.dir, relativePath), "utf-8");
9607
+ } catch {
9608
+ return void 0;
9609
+ }
9610
+ }
9611
+ readRelativeTranscriptDocument(relativePath) {
9612
+ const raw = this.readRelativeTextFile(relativePath);
9613
+ if (!raw) {
9614
+ return void 0;
9615
+ }
9616
+ return parseTranscriptDocument(raw);
9617
+ }
8943
9618
  saveIndex() {
8944
9619
  writeFileSync11(
8945
9620
  this.indexPath,
@@ -8951,6 +9626,9 @@ var RecordingStorage = class {
8951
9626
  }
8952
9627
  };
8953
9628
 
9629
+ // src/recording/index.ts
9630
+ init_transcript_document();
9631
+
8954
9632
  // src/recording/downloader.ts
8955
9633
  import { createWriteStream, existsSync as existsSync18, mkdirSync as mkdirSync11, statSync as statSync4, unlinkSync } from "fs";
8956
9634
  import { dirname as dirname6 } from "path";
@@ -9052,6 +9730,7 @@ function emitRecordingStatus(recordingId, storage, logger, notifyStatus, error,
9052
9730
  transfer_status: entry.status,
9053
9731
  audioFile: entry.audioFile,
9054
9732
  srtFile: entry.srtFile,
9733
+ transcriptDataFile: entry.transcriptDataFile,
9055
9734
  transcriptFile: entry.transcriptFile,
9056
9735
  transcript: extras?.transcript,
9057
9736
  summary: extras?.summary,
@@ -9150,12 +9829,21 @@ async function triggerTranscription(recordingId, storage, asrConfig, logger, opt
9150
9829
  recordingName: entry.metadata.name,
9151
9830
  durationSec: entry.metadata.duration_sec,
9152
9831
  createdAt: entry.metadata.created_at,
9832
+ transcriptDataDir: storage.getTranscriptDataDir(),
9153
9833
  transcriptsDir: storage.getTranscriptsDir(),
9834
+ summariesDir: storage.getSummariesDir(),
9154
9835
  recordingId,
9155
9836
  logger
9156
9837
  });
9157
9838
  if (result.ok && result.transcriptFilename) {
9839
+ if (result.transcriptDataFilename) {
9840
+ storage.setTranscriptDataFile(recordingId, result.transcriptDataFilename);
9841
+ }
9158
9842
  storage.setTranscriptFile(recordingId, result.transcriptFilename);
9843
+ if (result.summaryFilename) {
9844
+ storage.setSummaryFile(recordingId, result.summaryFilename);
9845
+ }
9846
+ storage.setTitle(recordingId, result.title);
9159
9847
  storage.updateStatus(recordingId, "transcribed");
9160
9848
  emitRecordingStatus(
9161
9849
  recordingId,
@@ -10737,7 +11425,8 @@ function registerStorageLifecycle(deps) {
10737
11425
  logger,
10738
11426
  lightRuleCtx,
10739
11427
  setStorage,
10740
- setRecordingStorage
11428
+ setRecordingStorage,
11429
+ onStorageReady
10741
11430
  } = deps;
10742
11431
  let storage = null;
10743
11432
  let recordingStorage = null;
@@ -10765,6 +11454,7 @@ function registerStorageLifecycle(deps) {
10765
11454
  if (ctx.stateDir) {
10766
11455
  lightRuleCtx.stateDir = ctx.stateDir;
10767
11456
  }
11457
+ onStorageReady?.();
10768
11458
  },
10769
11459
  async stop() {
10770
11460
  await storage?.close();
@@ -10847,9 +11537,14 @@ function createEmptyIngestResult() {
10847
11537
  ingested: 0,
10848
11538
  dedupedById: 0,
10849
11539
  dedupedByContent: 0,
10850
- invalid: 0
11540
+ invalid: 0,
11541
+ inserted: []
10851
11542
  };
10852
11543
  }
11544
+ function toIngestResponse(result) {
11545
+ const { inserted: _inserted, ...rest } = result;
11546
+ return rest;
11547
+ }
10853
11548
  function registerNotificationInterfaces(deps) {
10854
11549
  const {
10855
11550
  api,
@@ -10879,7 +11574,7 @@ function registerNotificationInterfaces(deps) {
10879
11574
  }
10880
11575
  const filtered = filterNotifications(items);
10881
11576
  const result = filtered.length ? await storage.ingest(filtered) : createEmptyIngestResult();
10882
- respond(true, result);
11577
+ respond(true, toIngestResponse(result));
10883
11578
  }
10884
11579
  );
10885
11580
  api.registerHttpRoute({
@@ -10919,7 +11614,7 @@ function registerNotificationInterfaces(deps) {
10919
11614
  const filtered = filterNotifications(body.notifications);
10920
11615
  const result = filtered.length ? await storage.ingest(filtered) : createEmptyIngestResult();
10921
11616
  res.writeHead(200, { "Content-Type": "application/json" });
10922
- res.end(JSON.stringify({ ok: true, ...result }));
11617
+ res.end(JSON.stringify({ ok: true, ...toIngestResponse(result) }));
10923
11618
  }
10924
11619
  });
10925
11620
  logger.info("Gateway \u901A\u77E5\u65B9\u6CD5\u5DF2\u6CE8\u518C: notifications.push");
@@ -10955,11 +11650,12 @@ function buildRecordingListItem(entry) {
10955
11650
  has_srt: !!entry.srtFile,
10956
11651
  has_transcript: !!entry.transcriptFile,
10957
11652
  audioFile: entry.audioFile,
11653
+ transcriptDataFile: entry.transcriptDataFile,
10958
11654
  transcriptFile: entry.transcriptFile,
10959
11655
  updatedAt: entry.updatedAt
10960
11656
  };
10961
11657
  }
10962
- function buildRecordingDetail(entry) {
11658
+ function buildRecordingDetail(entry, extras) {
10963
11659
  return {
10964
11660
  recordingId: entry.id,
10965
11661
  name: entry.metadata.name,
@@ -10973,7 +11669,12 @@ function buildRecordingDetail(entry) {
10973
11669
  transfer_status: entry.status,
10974
11670
  audioFile: entry.audioFile,
10975
11671
  srtFile: entry.srtFile,
11672
+ transcriptDataFile: entry.transcriptDataFile,
10976
11673
  transcriptFile: entry.transcriptFile,
11674
+ summaryFile: entry.summaryFile,
11675
+ title: entry.title,
11676
+ summary: extras?.summary,
11677
+ transcript: extras?.transcript,
10977
11678
  ingestedAt: entry.ingestedAt,
10978
11679
  updatedAt: entry.updatedAt
10979
11680
  };
@@ -11081,7 +11782,10 @@ function registerRecordingInterfaces(deps) {
11081
11782
  });
11082
11783
  return;
11083
11784
  }
11084
- const { recordingId } = params;
11785
+ const {
11786
+ recordingId,
11787
+ includeTranscriptContent
11788
+ } = params;
11085
11789
  if (!recordingId) {
11086
11790
  respond(false, null, {
11087
11791
  code: "INVALID_PARAMS",
@@ -11097,7 +11801,13 @@ function registerRecordingInterfaces(deps) {
11097
11801
  });
11098
11802
  return;
11099
11803
  }
11100
- respond(true, buildRecordingDetail(entry));
11804
+ respond(
11805
+ true,
11806
+ buildRecordingDetail(entry, {
11807
+ summary: recordingStorage.readSummary(recordingId),
11808
+ transcript: includeTranscriptContent ? recordingStorage.readTranscript(recordingId) : void 0
11809
+ })
11810
+ );
11101
11811
  }
11102
11812
  );
11103
11813
  registerGatewayMethod(
@@ -11376,6 +12086,7 @@ var index_default = {
11376
12086
  }
11377
12087
  broadcastFn("recording.status", event);
11378
12088
  }
12089
+ const lightRuleRegistry = new LightRuleRegistry(lightRuleCtx);
11379
12090
  registerStorageLifecycle({
11380
12091
  api,
11381
12092
  config,
@@ -11386,6 +12097,9 @@ var index_default = {
11386
12097
  },
11387
12098
  setRecordingStorage(nextRecordingStorage) {
11388
12099
  recordingStorage = nextRecordingStorage;
12100
+ },
12101
+ onStorageReady() {
12102
+ lightRuleRegistry.reload();
11389
12103
  }
11390
12104
  });
11391
12105
  const tunnelService = registerRelayTunnelLifecycle({
@@ -11411,7 +12125,7 @@ var index_default = {
11411
12125
  registerGatewayMethod: registerGatewayMethodWithBroadcastCapture,
11412
12126
  shouldBroadcastStatusOnHttp: () => !!broadcastFn
11413
12127
  });
11414
- registerLightRulesGateway(api, lightRuleCtx, logger, cacheBroadcast);
12128
+ registerLightRulesGateway(api, lightRuleRegistry, logger, cacheBroadcast);
11415
12129
  logger.info(
11416
12130
  "Gateway \u706F\u6548\u89C4\u5219\u65B9\u6CD5\u5DF2\u6CE8\u518C: lightrules.list / lightrules.create / lightrules.update / lightrules.delete"
11417
12131
  );