@yoooclaw/phone-notifications 1.12.2 → 1.12.3

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.cjs CHANGED
@@ -2959,7 +2959,7 @@ var import_node_path = require("node:path");
2959
2959
  var import_node_fs = require("node:fs");
2960
2960
  function readBuildInjectedVersion() {
2961
2961
  if (false) {}
2962
- const version = "1.12.2".trim();
2962
+ const version = "1.12.3".trim();
2963
2963
  return version || undefined;
2964
2964
  }
2965
2965
  function readPluginVersionFromPackageJson() {
@@ -5903,9 +5903,6 @@ function readPersistedEnvName() {
5903
5903
  return;
5904
5904
  }
5905
5905
  function loadEnvName() {
5906
- const fromEnvVar = process.env.PHONE_NOTIFICATIONS_ENV?.trim();
5907
- if (fromEnvVar && VALID_ENVS.has(fromEnvVar))
5908
- return fromEnvVar;
5909
5906
  const fromDotEnv = readPersistedEnvName();
5910
5907
  if (fromDotEnv)
5911
5908
  return fromDotEnv;
@@ -6560,7 +6557,7 @@ class LightRuleRegistry {
6560
6557
  }
6561
6558
 
6562
6559
  // src/index.ts
6563
- var import_node_path32 = require("node:path");
6560
+ var import_node_path31 = require("node:path");
6564
6561
 
6565
6562
  // src/update/channel.ts
6566
6563
  var PRODUCTION_UPDATE_BASE_URL = "https://artifact.yoooclaw.com/plugin";
@@ -7401,6 +7398,7 @@ function scheduleGatewayRestart(logger, deps = {}) {
7401
7398
  }
7402
7399
 
7403
7400
  // src/update/index.ts
7401
+
7404
7402
  var PLUGIN_ID2 = "phone-notifications";
7405
7403
  var VERSION_PATTERN2 = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
7406
7404
  function shouldSkipAutoRestart() {
@@ -7543,8 +7541,14 @@ async function reconcileSucceededUpdateRecord(api, logger, targetDir) {
7543
7541
  logger.warn(`更新已应用,但配置记录补写失败: ${String(err2)}`);
7544
7542
  }
7545
7543
  }
7546
- function resolveEffectiveCurrentVersion(targetDir) {
7547
- const installedVersion = readInstalledPackageVersion(targetDir);
7544
+ function resolveModulePackageJsonPath() {
7545
+ return import_node_path11.join(__dirname, "..", "package.json");
7546
+ }
7547
+ function resolveEffectiveCurrentVersion(targetDir, modulePackageJsonPath = resolveModulePackageJsonPath()) {
7548
+ const installedVersion = pickNewestVersion([
7549
+ readPackageVersionAt(import_node_path11.join(targetDir, "package.json")),
7550
+ readPackageVersionAt(modulePackageJsonPath)
7551
+ ]);
7548
7552
  if (installedVersion && isNewerVersion(installedVersion, PLUGIN_VERSION)) {
7549
7553
  return installedVersion;
7550
7554
  }
@@ -7554,9 +7558,9 @@ function resolveEffectiveCurrentVersion(targetDir) {
7554
7558
  }
7555
7559
  return PLUGIN_VERSION;
7556
7560
  }
7557
- function readInstalledPackageVersion(targetDir) {
7561
+ function readPackageVersionAt(packageJsonPath) {
7558
7562
  try {
7559
- const parsed = JSON.parse(import_node_fs11.readFileSync(import_node_path11.join(targetDir, "package.json"), "utf-8"));
7563
+ const parsed = JSON.parse(import_node_fs11.readFileSync(packageJsonPath, "utf-8"));
7560
7564
  const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
7561
7565
  return VERSION_PATTERN2.test(version) ? version : undefined;
7562
7566
  } catch {
@@ -7780,6 +7784,7 @@ function registerAutoUpdate(api, logger, config, getBroadcast, rememberBroadcast
7780
7784
  const targetDir = resolveTargetDir(api);
7781
7785
  const info = await checker.check();
7782
7786
  const current = resolveEffectiveCurrentVersion(targetDir);
7787
+ logger.info(`plugin.checkUpdate: running=${PLUGIN_VERSION} effective=${current} ` + `latest=${info?.latest ?? "-"} hasUpdate=${!!info} targetDir=${targetDir}`);
7783
7788
  const lastUpdateStatus = readLastUpdateStatus(targetDir);
7784
7789
  const lastUpdateNotice = lastUpdateStatus ? buildLastUpdateNotice(lastUpdateStatus) : undefined;
7785
7790
  if (!info) {
@@ -10789,7 +10794,7 @@ function pushCandidate(candidates, dir, label, onPath) {
10789
10794
  }
10790
10795
 
10791
10796
  // src/plugin/lifecycle.ts
10792
- var import_node_fs35 = require("node:fs");
10797
+ var import_node_fs34 = require("node:fs");
10793
10798
 
10794
10799
  // src/notification/app-name-map.ts
10795
10800
  var import_node_fs25 = require("node:fs");
@@ -10956,9 +10961,6 @@ function transition(from, to) {
10956
10961
  }
10957
10962
  return to;
10958
10963
  }
10959
- function canStartTranscription(status) {
10960
- return status === "synced" || status === "transcribe_failed" || status === "transcribed";
10961
- }
10962
10964
  class TransitionError extends Error {
10963
10965
  from;
10964
10966
  to;
@@ -11746,17 +11748,6 @@ async function downloadFile(url, destPath, logger, options) {
11746
11748
  }
11747
11749
  return { ok: false, error: lastError ?? "下载失败" };
11748
11750
  }
11749
- async function downloadRecordingFiles(ossAudioUrl, ossSrtUrl, audioDestPath, srtDestPath, logger, options) {
11750
- const audio = await downloadFile(ossAudioUrl, audioDestPath, logger, options);
11751
- let srt = null;
11752
- if (ossSrtUrl) {
11753
- srt = await downloadFile(ossSrtUrl, srtDestPath, logger, options);
11754
- if (!srt.ok) {
11755
- logger.warn(`[downloader] 打点文件下载失败(非致命): ${srt.error}`);
11756
- }
11757
- }
11758
- return { audio, srt };
11759
- }
11760
11751
  function formatBytes(bytes) {
11761
11752
  if (bytes < 1024)
11762
11753
  return `${bytes}B`;
@@ -11767,1010 +11758,194 @@ function formatBytes(bytes) {
11767
11758
  function sleep(ms) {
11768
11759
  return new Promise((resolve3) => setTimeout(resolve3, ms));
11769
11760
  }
11770
- // src/recording/asr.ts
11761
+ // src/recording/result-writer.ts
11771
11762
  var import_node_fs28 = require("node:fs");
11772
11763
  var import_node_path24 = require("node:path");
11773
- var DEFAULT_LONG_RECORDING_POLL_INTERVAL_MS = 2000;
11774
- var DEFAULT_LONG_RECORDING_MAX_POLL_ATTEMPTS = 3600;
11775
- var LONG_RECORDING_RUNNING_STATUSES = new Set(["PENDING", "RUNNING", "SUSPENDED"]);
11776
- var LONG_RECORDING_TERMINAL_FAILURE_STATUSES = new Set(["FAILED", "CANCELED", "UNKNOWN"]);
11777
- var DEFAULT_HTTP_MAX_ATTEMPTS = 3;
11778
- var DEFAULT_HTTP_RETRY_BACKOFF_MS = 2000;
11779
- function isAsrConfigured(config) {
11780
- return !!config && !validateAsrConfig(config);
11781
- }
11782
- function validateAsrConfig(config) {
11783
- if (!config?.mode) {
11784
- return "asr.mode is required";
11785
- }
11786
- switch (config.mode) {
11787
- case "api":
11788
- return;
11789
- case "local":
11790
- return '本地 Whisper 模式已停用,请改用 asr.mode = "api"';
11791
- case "yoooclaw":
11792
- return "YoooClaw ASR 尚未实现(P2)";
11793
- default:
11794
- return `未知的 ASR mode: ${config.mode}`;
11795
- }
11796
- }
11797
- async function initializeAsr(config, dataDir, logger) {
11798
- const validationError = validateAsrConfig(config);
11799
- if (validationError) {
11800
- return {
11801
- ok: false,
11802
- mode: config.mode,
11803
- error: validationError
11804
- };
11764
+ function handleRecordingResultWrite(params, storage2, logger, options = {}) {
11765
+ const recordingId = normalizeRequiredText(params.recordingId, "recordingId");
11766
+ if (!params.transcript && !params.summary) {
11767
+ throw new Error("transcript or summary is required");
11805
11768
  }
11806
- switch (config.mode) {
11807
- case "api": {
11808
- const endpoint = resolveModelProxyLongRecordingSubmitEndpoint(config.api);
11809
- const keyConfigured = hasText(config.api?.apiKey) || hasText(loadApiKey());
11810
- if (!keyConfigured) {
11811
- return {
11812
- ok: false,
11813
- mode: "api",
11814
- provider: "model-proxy",
11815
- endpoint,
11816
- language: config.api?.language ?? "auto",
11817
- keyConfigured: false,
11818
- error: "API Key 未设置,请在本次 asr.api.apiKey 中传入,或先写入 credentials.json / 执行 ntf auth set-api-key <apiKey>"
11819
- };
11820
- }
11821
- return {
11822
- ok: true,
11823
- mode: "api",
11824
- provider: "model-proxy",
11825
- endpoint,
11826
- language: config.api?.language ?? "auto",
11827
- keyConfigured: true
11828
- };
11769
+ let entry = storage2.findById(recordingId);
11770
+ if (!entry) {
11771
+ storage2.ingest(recordingId, buildPlaceholderMetadata(recordingId, params));
11772
+ entry = storage2.findById(recordingId);
11773
+ if (!entry) {
11774
+ throw new Error(`Recording not found: ${recordingId}`);
11829
11775
  }
11830
- case "local":
11831
- return {
11832
- ok: false,
11833
- mode: "local",
11834
- error: '本地 Whisper 模式已停用,请改用 asr.mode = "api"'
11835
- };
11836
- case "yoooclaw":
11837
- return {
11838
- ok: false,
11839
- mode: "yoooclaw",
11840
- error: "YoooClaw ASR 尚未实现(P2)"
11841
- };
11842
- default:
11843
- return {
11844
- ok: false,
11845
- mode: config.mode,
11846
- error: `未知的 ASR mode: ${config.mode}`
11847
- };
11776
+ logger.info(`[recording-result] 录音不存在,已按结果写入新建: ${recordingId}`);
11848
11777
  }
11849
- }
11850
- async function transcribeAudio(audioFilePath, config, logger, options = {}) {
11851
- if (!import_node_fs28.existsSync(audioFilePath)) {
11852
- return { ok: false, error: `音频文件不存在: ${audioFilePath}` };
11778
+ let transcript;
11779
+ let title = entry.title ?? entry.metadata.name ?? recordingId;
11780
+ let summaryText;
11781
+ if (params.transcript) {
11782
+ const written = writeTranscript(recordingId, entry.metadata, params.transcript, storage2, logger);
11783
+ transcript = written.transcript;
11784
+ title = written.title;
11853
11785
  }
11854
- logger.info(`[asr] 开始转写: mode=${config.mode}, file=${audioFilePath}`);
11855
- try {
11856
- switch (config.mode) {
11857
- case "api":
11858
- return await transcribeWithModelProxy(options.audioOssUrl, options.audioDurationMs, config.api, logger);
11859
- case "local":
11860
- return {
11861
- ok: false,
11862
- error: '本地 Whisper 模式已停用,请改用 asr.mode = "api"'
11863
- };
11864
- case "yoooclaw":
11865
- return { ok: false, error: "YoooClaw ASR 尚未实现(P2)" };
11866
- default:
11867
- return {
11868
- ok: false,
11869
- error: `未知的 ASR mode: ${config.mode}`
11870
- };
11871
- }
11872
- } catch (err2) {
11873
- const msg = err2?.message ?? String(err2);
11874
- logger.error(`[asr] 转写异常: ${msg}`);
11875
- return { ok: false, error: msg };
11786
+ if (params.summary) {
11787
+ summaryText = writeSummary(recordingId, params.summary, storage2, logger);
11876
11788
  }
11877
- }
11878
- function buildTranscriptMarkdown(result, markers, recordingName, durationSec, createdAt) {
11879
- const summary = result.summary ?? recordingName.slice(0, 10);
11880
- const lines = [];
11881
- lines.push(`# ${summary}`);
11882
- lines.push("");
11883
- lines.push(`> 录音名称:${recordingName}`);
11884
- lines.push(`> 时长:${formatDuration(durationSec)}`);
11885
- lines.push(`> 创建时间:${createdAt}`);
11886
- if (markers.length > 0) {
11887
- lines.push(`> 关键点数:${markers.length}`);
11789
+ const updated = storage2.markResultWritten(recordingId);
11790
+ emitResultStatus(recordingId, storage2, logger, options.notifyStatus, {
11791
+ transcript,
11792
+ summary: summaryText ?? storage2.readSummary(recordingId),
11793
+ title
11794
+ });
11795
+ const ossUrl = normalizeText(params.ossUrl);
11796
+ if (ossUrl) {
11797
+ downloadAudioInBackground(recordingId, ossUrl, storage2, logger, options);
11888
11798
  }
11889
- lines.push("");
11890
- lines.push("---");
11891
- lines.push("");
11892
- if (result.segments && result.segments.length > 0) {
11893
- const sortedMarkers = [...markers].sort((a, b) => a.timestamp_ms - b.timestamp_ms);
11894
- let markerIdx = 0;
11895
- for (const seg of result.segments) {
11896
- while (markerIdx < sortedMarkers.length && sortedMarkers[markerIdx].timestamp_ms <= seg.start_ms) {
11897
- const m = sortedMarkers[markerIdx];
11898
- lines.push(`**[关键点 ${formatTimestamp(m.timestamp_ms)}]**`);
11899
- lines.push("");
11900
- markerIdx++;
11901
- }
11902
- const segmentText = formatTranscriptSegmentText(seg);
11903
- if (segmentText) {
11904
- lines.push(segmentText);
11905
- lines.push("");
11906
- }
11907
- }
11908
- while (markerIdx < sortedMarkers.length) {
11909
- const m = sortedMarkers[markerIdx];
11910
- lines.push(`**[关键点 ${formatTimestamp(m.timestamp_ms)}]**`);
11911
- lines.push("");
11912
- markerIdx++;
11913
- }
11914
- } else if (result.text) {
11915
- if (markers.length > 0) {
11916
- lines.push("### 关键点");
11917
- lines.push("");
11918
- for (const m of markers) {
11919
- lines.push(`- **[关键点 ${formatTimestamp(m.timestamp_ms)}]**`);
11920
- }
11921
- lines.push("");
11922
- lines.push("---");
11923
- lines.push("");
11799
+ return {
11800
+ ok: true,
11801
+ recordingId,
11802
+ transfer_status: updated.status,
11803
+ stored: {
11804
+ transcriptDataFile: updated.transcriptDataFile,
11805
+ transcriptFile: updated.transcriptFile,
11806
+ summaryFile: updated.summaryFile
11924
11807
  }
11925
- lines.push(result.text);
11926
- lines.push("");
11927
- }
11928
- return lines.join(`
11929
- `);
11808
+ };
11930
11809
  }
11931
- function extractSummary(text) {
11932
- const firstLine = text.split(/[。!?\n]/).find((s) => s.trim().length > 0);
11933
- if (!firstLine)
11934
- return "录音转写";
11935
- const trimmed = firstLine.trim();
11936
- return trimmed.length <= 10 ? trimmed : trimmed.slice(0, 10);
11810
+ function buildPlaceholderMetadata(recordingId, params) {
11811
+ return {
11812
+ name: normalizeText(params.transcript?.title) ?? recordingId,
11813
+ duration_sec: 0,
11814
+ file_size_bytes: 0,
11815
+ created_at: normalizeText(params.transcript?.generatedAt) ?? new Date().toISOString(),
11816
+ oss_audio_url: normalizeText(params.ossUrl) ?? "",
11817
+ markers: [],
11818
+ transfer_status: "synced"
11819
+ };
11937
11820
  }
11938
- async function runTranscriptionWorkflow(params) {
11939
- const {
11940
- audioFilePath,
11941
- audioOssUrl,
11942
- config,
11943
- markers,
11944
- recordingName,
11945
- durationSec,
11946
- createdAt,
11947
- transcriptsDir,
11948
- transcriptDataDir,
11949
- summariesDir,
11950
- recordingId,
11951
- logger
11952
- } = params;
11953
- const result = await transcribeAudio(audioFilePath, config, logger, {
11954
- audioOssUrl,
11955
- audioDurationMs: Math.max(0, Math.round(durationSec * 1000))
11956
- });
11957
- if (!result.ok) {
11958
- return { ok: false, error: result.error };
11959
- }
11960
- const title = normalizeOptionalText3(result.summary) ? normalizeOptionalText3(result.summary) : extractSummary(result.text ?? "");
11961
- const summary = result.summaryText ?? "";
11962
- result.summary = title;
11963
- const transcriptData = buildTranscriptDocument({
11821
+ function writeTranscript(recordingId, recording, transcript, storage2, logger) {
11822
+ const title = normalizeText(transcript.title) ?? normalizeText(recording.name) ?? recordingId;
11823
+ const segments = normalizeSegments2(transcript.segments);
11824
+ const text = normalizePossiblyEmptyText2(transcript.text) ?? joinSegmentTexts(segments) ?? normalizePossiblyEmptyText2(transcript.markdown) ?? "";
11825
+ const transcriptDocument = buildTranscriptDocument({
11964
11826
  recordingId,
11965
- generatedAt: new Date().toISOString(),
11966
- source: result.sourceInfo ?? {
11967
- provider: config.mode === "api" ? "model-proxy" : config.mode
11827
+ generatedAt: normalizeText(transcript.generatedAt) ?? new Date().toISOString(),
11828
+ source: {
11829
+ provider: normalizeText(transcript.source?.provider) ?? "app",
11830
+ taskId: normalizeText(transcript.source?.taskId),
11831
+ requestId: normalizeText(transcript.source?.requestId),
11832
+ status: normalizeText(transcript.source?.status),
11833
+ delivery: "result-write"
11968
11834
  },
11969
11835
  title,
11970
- category: result.category,
11971
- summary,
11972
- text: result.text,
11973
- segments: (result.segments ?? []).map((segment) => ({
11974
- text: segment.text,
11975
- startMs: segment.start_ms,
11976
- endMs: segment.end_ms,
11977
- speakerId: segment.speaker_id
11978
- })),
11979
- raw: result.rawResponse
11836
+ category: normalizeText(transcript.category),
11837
+ summary: normalizePossiblyEmptyText2(transcript.brief),
11838
+ text,
11839
+ segments,
11840
+ raw: transcript
11980
11841
  });
11981
- const markdown = buildTranscriptMarkdown(result, markers, recordingName, durationSec, createdAt);
11982
- const transcriptDataFilename = buildTranscriptDataFilename(recordingId);
11983
- const transcriptDataPath = import_node_path24.join(transcriptDataDir, transcriptDataFilename);
11984
- import_node_fs28.writeFileSync(transcriptDataPath, JSON.stringify(transcriptData, null, 2), "utf-8");
11985
- logger.info(`[asr] 转写 JSON 已写入: ${transcriptDataPath}`);
11986
- const safeSummary = title.replace(/[/\\:*?"<>|]/g, "").trim().slice(0, 20);
11987
- const filename = safeSummary ? `${recordingId}_${safeSummary}.md` : `${recordingId}.md`;
11988
- const filePath = import_node_path24.join(transcriptsDir, filename);
11989
- import_node_fs28.writeFileSync(filePath, markdown, "utf-8");
11990
- logger.info(`[asr] 转写文本已写入: ${filePath}`);
11991
- let summaryFilename;
11992
- if (summary) {
11993
- summaryFilename = `${recordingId}.md`;
11994
- const summaryFilePath = import_node_path24.join(summariesDir, summaryFilename);
11995
- import_node_fs28.writeFileSync(summaryFilePath, summary, "utf-8");
11996
- logger.info(`[asr] 摘要文本已写入: ${summaryFilePath}`);
11997
- }
11842
+ const transcriptDataFilename = storage2.buildTranscriptDataFilename(recordingId);
11843
+ import_node_fs28.writeFileSync(storage2.getTranscriptDataFilePath(recordingId), JSON.stringify(transcriptDocument, null, 2), "utf-8");
11844
+ storage2.setTranscriptDataFile(recordingId, transcriptDataFilename);
11845
+ const transcriptFilename = storage2.buildTranscriptFilename(recordingId, title);
11846
+ const markdown = normalizePossiblyEmptyText2(transcript.markdown) ?? buildTranscriptMarkdown(recording, title, text, segments);
11847
+ import_node_fs28.writeFileSync(import_node_path24.join(storage2.getTranscriptsDir(), transcriptFilename), ensureTrailingNewline(markdown), "utf-8");
11848
+ storage2.setTranscriptFile(recordingId, transcriptFilename);
11849
+ storage2.setTitle(recordingId, title);
11850
+ logger.info(`[recording-result] 转录文本已写入: ${recordingId}`);
11998
11851
  return {
11999
- ok: true,
12000
- transcriptFilename: filename,
12001
- transcriptDataFilename,
12002
- summaryFilename,
12003
- transcript: extractSourceTextListFromDocument(transcriptData),
12004
- summary,
12005
- title
11852
+ title,
11853
+ transcript: extractSourceTextListFromDocument(transcriptDocument) ?? []
12006
11854
  };
12007
11855
  }
12008
- async function transcribeWithModelProxy(audioOssUrl, audioDurationMs, apiConfig, logger) {
12009
- const normalizedAudioOssUrl = normalizeOptionalText3(audioOssUrl);
12010
- if (!normalizedAudioOssUrl) {
12011
- return { ok: false, error: "API 模式缺少 audioOssUrl,无法调用 model-proxy" };
12012
- }
12013
- const apiKey = resolveModelProxyApiKey(apiConfig);
12014
- const submitEndpoint = resolveModelProxyLongRecordingSubmitEndpoint(apiConfig);
12015
- const submitBody = buildLongRecordingSubmitRequest(normalizedAudioOssUrl, apiConfig);
12016
- logger.info(`[asr-submit] 提交长录音任务: endpoint=${submitEndpoint}, body=${stringifyForLog(submitBody) ?? "{}"}`);
12017
- let res;
12018
- try {
12019
- res = await fetchWithRetry(submitEndpoint, {
12020
- method: "POST",
12021
- headers: {
12022
- "Content-Type": "application/json",
12023
- "X-Api-Key-Id": apiKey
12024
- },
12025
- body: JSON.stringify(submitBody)
12026
- }, { logger, context: "asr-submit" });
12027
- } catch (err2) {
12028
- const msg = err2?.message ?? String(err2);
12029
- logger.error(`[asr-submit] 提交长录音任务网络异常 (已重试): ${msg}`);
12030
- return {
12031
- ok: false,
12032
- error: `Model Proxy ASR submit network error: ${msg}`
12033
- };
12034
- }
12035
- if (!res.ok) {
12036
- const errText = await res.text();
12037
- logger.error(`[asr-submit-response] 提交长录音任务失败: status=${res.status}, body=${errText.slice(0, 500)}`);
12038
- return {
12039
- ok: false,
12040
- error: `Model Proxy ASR error: ${res.status} ${errText.slice(0, 200)}`
12041
- };
12042
- }
12043
- const raw = await res.json();
12044
- logger.info(`[asr-submit-response] 提交长录音任务响应: ${stringifyForLog(raw) ?? "{}"}`);
12045
- const submitEnvelopeError = buildModelProxyEnvelopeError(raw);
12046
- if (submitEnvelopeError) {
12047
- logger.error(`[asr-submit-response] 提交长录音任务失败: ${submitEnvelopeError}`);
12048
- return {
12049
- ok: false,
12050
- error: `Model Proxy ASR 提交失败: ${submitEnvelopeError}`
12051
- };
12052
- }
12053
- const data = unwrapResponse(raw);
12054
- const taskId = normalizeOptionalText3(data?.taskId);
12055
- const requestId = normalizeOptionalText3(data?.requestId);
12056
- const status = normalizeLongRecordingStatus(data?.status);
12057
- if (!taskId) {
12058
- return {
12059
- ok: false,
12060
- error: "Model Proxy ASR 响应缺少 taskId"
12061
- };
12062
- }
12063
- logger.info(`[asr] Model Proxy 长录音任务已提交: taskId=${taskId}, status=${status ?? "UNKNOWN"}, requestId=${requestId ?? "n/a"}`);
12064
- if (status && LONG_RECORDING_TERMINAL_FAILURE_STATUSES.has(status)) {
12065
- return {
12066
- ok: false,
12067
- error: buildLongRecordingStatusError(data, status)
12068
- };
11856
+ function writeSummary(recordingId, summary, storage2, logger) {
11857
+ const markdown = normalizePossiblyEmptyText2(summary.markdown) ?? buildStructuredSummaryMarkdown(summary.structured);
11858
+ if (markdown === undefined) {
11859
+ return;
12069
11860
  }
12070
- return await pollLongRecordingTaskResult({
12071
- apiKey,
12072
- taskId,
12073
- initialRequestId: requestId,
12074
- audioDurationMs,
12075
- apiConfig,
12076
- logger
12077
- });
11861
+ const summaryFilename = storage2.buildSummaryFilename(recordingId);
11862
+ import_node_fs28.writeFileSync(storage2.getSummaryFilePath(recordingId), ensureTrailingNewline(markdown), "utf-8");
11863
+ storage2.setSummaryFile(recordingId, summaryFilename);
11864
+ logger.info(`[recording-result] 总结已写入: ${recordingId}`);
11865
+ return markdown.trim();
12078
11866
  }
12079
- function resolveModelProxyLongRecordingSubmitEndpoint(apiConfig) {
12080
- return apiConfig?.endpoint?.trim() || getEnvUrls().modelProxyLongRecordingSubmitTaskUrl;
11867
+ function downloadAudioInBackground(recordingId, ossUrl, storage2, logger, options) {
11868
+ const audioDestPath = storage2.getAudioFilePath(recordingId, ossUrl);
11869
+ logger.info(`[recording-result] 开始下载音频: ${recordingId}, audio=${ossUrl}`);
11870
+ downloadFile(ossUrl, audioDestPath, logger).then((result) => {
11871
+ if (!result.ok) {
11872
+ const error = `音频下载失败: ${result.error}`;
11873
+ logger.error(`[recording-result] ${error}: ${recordingId}`);
11874
+ storage2.setLastError(recordingId, error);
11875
+ emitDownloadStatus(recordingId, storage2, logger, options.notifyStatus);
11876
+ return;
11877
+ }
11878
+ storage2.setAudioFile(recordingId, storage2.buildAudioFilename(recordingId, ossUrl));
11879
+ logger.info(`[recording-result] 音频已更新: ${recordingId}`);
11880
+ emitDownloadStatus(recordingId, storage2, logger, options.notifyStatus);
11881
+ }).catch((err2) => {
11882
+ const error = `音频下载异常: ${err2?.message ?? err2}`;
11883
+ logger.error(`[recording-result] ${error}: ${recordingId}`);
11884
+ storage2.setLastError(recordingId, error);
11885
+ emitDownloadStatus(recordingId, storage2, logger, options.notifyStatus);
11886
+ });
12081
11887
  }
12082
- function resolveModelProxyApiKey(apiConfig) {
12083
- const requestApiKey = normalizeOptionalText3(apiConfig?.apiKey);
12084
- return normalizeApiKeyHeaderValue(requestApiKey ?? requireApiKey());
11888
+ function emitDownloadStatus(recordingId, storage2, logger, notifyStatus) {
11889
+ const entry = storage2.findById(recordingId);
11890
+ if (!entry)
11891
+ return;
11892
+ emitResultStatus(recordingId, storage2, logger, notifyStatus, {
11893
+ summary: storage2.readSummary(recordingId),
11894
+ title: entry.title ?? entry.metadata.name ?? recordingId
11895
+ });
12085
11896
  }
12086
- function resolveModelProxyLongRecordingQueryTaskResultBaseUrl(apiConfig) {
12087
- const customEndpoint = apiConfig?.endpoint?.trim();
12088
- if (customEndpoint) {
12089
- return deriveLongRecordingQueryTaskResultBaseUrl(customEndpoint);
11897
+ function emitResultStatus(recordingId, storage2, logger, notifyStatus, extras) {
11898
+ if (!notifyStatus)
11899
+ return;
11900
+ const entry = storage2.findById(recordingId);
11901
+ if (!entry)
11902
+ return;
11903
+ try {
11904
+ notifyStatus({
11905
+ recordingId: entry.id,
11906
+ transfer_status: entry.status,
11907
+ audioFile: entry.audioFile,
11908
+ srtFile: entry.srtFile,
11909
+ transcriptDataFile: entry.transcriptDataFile,
11910
+ transcriptFile: entry.transcriptFile,
11911
+ summaryFile: entry.summaryFile,
11912
+ transcript: extras.transcript,
11913
+ summary: extras.summary,
11914
+ title: extras.title,
11915
+ updatedAt: entry.updatedAt,
11916
+ error: entry.lastError
11917
+ });
11918
+ } catch (err2) {
11919
+ logger.error(`[recording-result] 状态事件发送失败: ${recordingId}, ${err2?.message ?? err2}`);
12090
11920
  }
12091
- return getEnvUrls().modelProxyLongRecordingQueryTaskResultBaseUrl;
12092
11921
  }
12093
- function deriveLongRecordingQueryTaskResultBaseUrl(endpoint) {
12094
- const trimmed = endpoint.replace(/\/+$/, "");
12095
- if (trimmed.endsWith("/submit-task")) {
12096
- return `${trimmed.slice(0, -"/submit-task".length)}/query-task-result`;
12097
- }
12098
- return trimmed;
11922
+ function normalizeSegments2(segments) {
11923
+ if (!Array.isArray(segments))
11924
+ return [];
11925
+ return segments.flatMap((segment) => {
11926
+ const text = normalizeText(segment.text);
11927
+ if (!text)
11928
+ return [];
11929
+ return [{
11930
+ text,
11931
+ startMs: typeof segment.startMs === "number" ? segment.startMs : undefined,
11932
+ endMs: typeof segment.endMs === "number" ? segment.endMs : undefined,
11933
+ speakerId: Number.isInteger(segment.speakerId) ? segment.speakerId : undefined
11934
+ }];
11935
+ });
12099
11936
  }
12100
- function buildLongRecordingSubmitRequest(audioOssUrl, apiConfig) {
12101
- const body = {
12102
- audioOssUrl
12103
- };
12104
- if (apiConfig?.language?.trim()) {
12105
- body.language = apiConfig.language.trim();
12106
- }
12107
- if (typeof apiConfig?.enableNormalization === "boolean") {
12108
- body.enableNormalization = apiConfig.enableNormalization;
12109
- }
12110
- return body;
11937
+ function joinSegmentTexts(segments) {
11938
+ const text = segments.map((segment) => segment.text).filter(Boolean).join(`
11939
+
11940
+ `).trim();
11941
+ return text || undefined;
12111
11942
  }
12112
- async function pollLongRecordingTaskResult(params) {
12113
- const {
12114
- apiKey,
12115
- taskId,
12116
- initialRequestId,
12117
- audioDurationMs,
12118
- apiConfig,
12119
- logger
12120
- } = params;
12121
- const queryBaseUrl = resolveModelProxyLongRecordingQueryTaskResultBaseUrl(apiConfig);
12122
- let lastStatus;
12123
- const pollIntervalMs = getPollIntervalMs();
12124
- for (let attempt = 1;attempt <= DEFAULT_LONG_RECORDING_MAX_POLL_ATTEMPTS; attempt++) {
12125
- const queryUrl = `${queryBaseUrl}/${encodeURIComponent(taskId)}`;
12126
- let res;
12127
- try {
12128
- res = await fetch(queryUrl, {
12129
- method: "GET",
12130
- headers: {
12131
- "X-Api-Key-Id": apiKey
12132
- }
12133
- });
12134
- } catch (err2) {
12135
- const msg = err2?.message ?? String(err2);
12136
- logger.warn(`[asr-query] 长录音任务查询网络异常: taskId=${taskId}, attempt=${attempt}, error=${msg} — 等待下次轮询`);
12137
- if (attempt < DEFAULT_LONG_RECORDING_MAX_POLL_ATTEMPTS) {
12138
- await sleep2(pollIntervalMs);
12139
- continue;
12140
- }
12141
- return {
12142
- ok: false,
12143
- error: `Model Proxy ASR query network error: ${msg}`
12144
- };
12145
- }
12146
- if (!res.ok) {
12147
- const errText = await res.text().catch(() => "");
12148
- if (isRetryableHttpStatus(res.status)) {
12149
- logger.warn(`[asr-query] 长录音任务查询暂时失败: taskId=${taskId}, attempt=${attempt}, status=${res.status}, body=${errText.slice(0, 200)} — 等待下次轮询`);
12150
- if (attempt < DEFAULT_LONG_RECORDING_MAX_POLL_ATTEMPTS) {
12151
- await sleep2(pollIntervalMs);
12152
- continue;
12153
- }
12154
- } else {
12155
- logger.error(`[asr-query-response] 长录音任务查询失败: taskId=${taskId}, attempt=${attempt}, status=${res.status}, body=${errText.slice(0, 500)}`);
12156
- }
12157
- return {
12158
- ok: false,
12159
- error: `Model Proxy ASR query error: ${res.status} ${errText.slice(0, 200)}`
12160
- };
12161
- }
12162
- const raw = await res.json();
12163
- const queryEnvelopeError = buildModelProxyEnvelopeError(raw);
12164
- if (queryEnvelopeError) {
12165
- logger.error(`[asr-query-response] 长录音任务查询失败: taskId=${taskId}, attempt=${attempt}, ${queryEnvelopeError}`);
12166
- return {
12167
- ok: false,
12168
- error: `Model Proxy ASR 查询失败: ${queryEnvelopeError}`
12169
- };
12170
- }
12171
- const data = unwrapResponse(raw);
12172
- const status = normalizeLongRecordingStatus(data?.status) ?? "UNKNOWN";
12173
- const requestId = normalizeOptionalText3(data?.requestId) ?? initialRequestId;
12174
- if (status !== lastStatus) {
12175
- logger.info(`[asr-query-response] 长录音任务查询响应: taskId=${taskId}, attempt=${attempt}, body=${stringifyForLog(raw) ?? "{}"}`);
12176
- logger.info(`[asr] Model Proxy 长录音任务状态: taskId=${taskId}, status=${status}, attempt=${attempt}, requestId=${requestId ?? "n/a"}`);
12177
- lastStatus = status;
12178
- }
12179
- if (status === "SUCCEEDED") {
12180
- return buildLongRecordingSuccessResult(taskId, requestId, data, audioDurationMs, logger);
12181
- }
12182
- if (LONG_RECORDING_TERMINAL_FAILURE_STATUSES.has(status)) {
12183
- return {
12184
- ok: false,
12185
- error: buildLongRecordingStatusError(data, status)
12186
- };
12187
- }
12188
- if (!LONG_RECORDING_RUNNING_STATUSES.has(status)) {
12189
- return {
12190
- ok: false,
12191
- error: `Model Proxy ASR 返回未知任务状态: ${status}`
12192
- };
12193
- }
12194
- if (attempt < DEFAULT_LONG_RECORDING_MAX_POLL_ATTEMPTS) {
12195
- await sleep2(pollIntervalMs);
12196
- }
12197
- }
12198
- return {
12199
- ok: false,
12200
- error: `Model Proxy ASR 轮询超时: taskId=${taskId}, waited=${DEFAULT_LONG_RECORDING_MAX_POLL_ATTEMPTS * pollIntervalMs}ms`
12201
- };
12202
- }
12203
- function buildLongRecordingSuccessResult(taskId, requestId, data, audioDurationMs, logger) {
12204
- const sourceTextList = normalizeLongRecordingSourceTextList(data?.recordResult?.sourceTextList);
12205
- const listResult = extractLongRecordingTextFromList(sourceTextList, audioDurationMs);
12206
- const sourceText = normalizeOptionalText3(data?.recordResult?.sourceText);
12207
- const summaryText = normalizeOptionalText3(data?.recordResult?.summaryResult) ?? "";
12208
- const title = normalizeOptionalText3(data?.recordResult?.title);
12209
- const category = normalizeOptionalText3(data?.recordResult?.category);
12210
- const text = listResult.text ?? sourceText ?? summaryText;
12211
- const status = normalizeLongRecordingStatus(data?.status) ?? "SUCCEEDED";
12212
- if (!listResult.text && !sourceText && summaryText) {
12213
- logger.warn(`[asr] Model Proxy 长录音结果缺少 sourceTextList/sourceText,已回退使用 summaryResult 作为转写文本: taskId=${taskId}`);
12214
- }
12215
- logger.info(`[asr] Model Proxy 长录音转写完成: taskId=${taskId}, requestId=${requestId ?? "n/a"}, chars=${text.length}`);
12216
- return {
12217
- ok: true,
12218
- text,
12219
- segments: listResult.segments,
12220
- summary: title,
12221
- summaryText,
12222
- category,
12223
- sourceInfo: {
12224
- provider: "model-proxy",
12225
- taskId,
12226
- requestId,
12227
- status
12228
- },
12229
- rawResponse: data
12230
- };
12231
- }
12232
- function buildLongRecordingStatusError(data, status) {
12233
- const errorMessage = normalizeOptionalText3(data?.errorMessage);
12234
- return errorMessage ? `Model Proxy ASR ${status}: ${errorMessage}` : `Model Proxy ASR ${status}`;
12235
- }
12236
- function buildModelProxyEnvelopeError(payload) {
12237
- if (!payload || typeof payload !== "object" || !("data" in payload)) {
12238
- return;
12239
- }
12240
- const envelope = payload;
12241
- const explicitFailure = envelope.success === false;
12242
- const message = normalizeOptionalText3(envelope.message);
12243
- if (!explicitFailure && (!message || envelope.data)) {
12244
- return;
12245
- }
12246
- const code = normalizeOptionalCode(envelope.code);
12247
- if (code && message) {
12248
- return `${code} ${message}`;
12249
- }
12250
- return message ?? code ?? "response envelope indicates failure";
12251
- }
12252
- function unwrapResponse(payload) {
12253
- if (payload && typeof payload === "object" && "data" in payload && payload.data && typeof payload.data === "object") {
12254
- return payload.data;
12255
- }
12256
- return payload;
12257
- }
12258
- function normalizeApiKeyHeaderValue(apiKey) {
12259
- return apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey;
12260
- }
12261
- function normalizeLongRecordingStatus(status) {
12262
- return typeof status === "string" && status.trim() ? status.trim().toUpperCase() : undefined;
12263
- }
12264
- function normalizeOptionalText3(value) {
12265
- return typeof value === "string" && value.trim() ? value.trim() : undefined;
12266
- }
12267
- function normalizeOptionalCode(value) {
12268
- if (typeof value === "string" && value.trim()) {
12269
- return value.trim();
12270
- }
12271
- if (typeof value === "number" && Number.isFinite(value)) {
12272
- return String(value);
12273
- }
12274
- return;
12275
- }
12276
- function normalizeOptionalInteger2(value) {
12277
- return Number.isInteger(value) ? Number(value) : undefined;
12278
- }
12279
- function normalizeOptionalNonNegativeNumber(value) {
12280
- return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
12281
- }
12282
- function normalizeLongRecordingSourceTextList(value) {
12283
- if (!Array.isArray(value)) {
12284
- return [];
12285
- }
12286
- return value.flatMap((item) => {
12287
- if (!item || typeof item !== "object") {
12288
- return [];
12289
- }
12290
- const record = item;
12291
- const content = normalizeOptionalText3(record.content);
12292
- if (!content) {
12293
- return [];
12294
- }
12295
- return [{
12296
- content,
12297
- speakerId: normalizeOptionalInteger2(record.speakerId),
12298
- startTime: normalizeOptionalNonNegativeNumber(record.startTime),
12299
- endTime: normalizeOptionalNonNegativeNumber(record.endTime)
12300
- }];
12301
- });
12302
- }
12303
- function extractLongRecordingTextFromList(items, finalFallbackEndMs) {
12304
- if (items.length === 0) {
12305
- return {
12306
- segments: []
12307
- };
12308
- }
12309
- const segments = items.map((item, index) => {
12310
- const startMs = item.startTime ?? 0;
12311
- const explicitEndMs = item.endTime;
12312
- const nextStart = items[index + 1]?.startTime;
12313
- const fallbackEndMs = index === items.length - 1 ? normalizeOptionalNonNegativeNumber(finalFallbackEndMs) : undefined;
12314
- const endMs = explicitEndMs ?? nextStart ?? fallbackEndMs ?? startMs;
12315
- return {
12316
- start_ms: startMs,
12317
- end_ms: Math.max(startMs, endMs),
12318
- text: item.content,
12319
- speaker_id: item.speakerId
12320
- };
12321
- });
12322
- return {
12323
- text: items.map((item) => item.content).join(`
12324
-
12325
- `),
12326
- segments
12327
- };
12328
- }
12329
- function stringifyForLog(value, maxLength = 500) {
12330
- if (value == null) {
12331
- return;
12332
- }
12333
- if (typeof value === "string") {
12334
- const trimmed = value.trim();
12335
- return trimmed ? trimmed.slice(0, maxLength) : undefined;
12336
- }
12337
- try {
12338
- const serialized = JSON.stringify(value);
12339
- return serialized ? serialized.slice(0, maxLength) : undefined;
12340
- } catch {
12341
- return String(value).slice(0, maxLength);
12342
- }
12343
- }
12344
- async function sleep2(ms) {
12345
- await new Promise((resolve3) => setTimeout(resolve3, ms));
12346
- }
12347
- function isRetryableHttpStatus(status) {
12348
- return status === 429 || status >= 500;
12349
- }
12350
- function getHttpRetryBackoffMs() {
12351
- const raw = process.env.OPENCLAW_ASR_HTTP_RETRY_BACKOFF_MS;
12352
- if (raw) {
12353
- const parsed = Number(raw);
12354
- if (Number.isFinite(parsed) && parsed >= 0) {
12355
- return parsed;
12356
- }
12357
- }
12358
- return DEFAULT_HTTP_RETRY_BACKOFF_MS;
12359
- }
12360
- function getPollIntervalMs() {
12361
- const raw = process.env.OPENCLAW_ASR_POLL_INTERVAL_MS;
12362
- if (raw) {
12363
- const parsed = Number(raw);
12364
- if (Number.isFinite(parsed) && parsed >= 0) {
12365
- return parsed;
12366
- }
12367
- }
12368
- return DEFAULT_LONG_RECORDING_POLL_INTERVAL_MS;
12369
- }
12370
- async function fetchWithRetry(url, init, options) {
12371
- const maxAttempts = options.maxAttempts ?? DEFAULT_HTTP_MAX_ATTEMPTS;
12372
- const baseBackoff = options.backoffMs ?? getHttpRetryBackoffMs();
12373
- let lastError;
12374
- for (let attempt = 1;attempt <= maxAttempts; attempt++) {
12375
- try {
12376
- const res = await fetch(url, init);
12377
- if (isRetryableHttpStatus(res.status) && attempt < maxAttempts) {
12378
- const errText = await res.text().catch(() => "");
12379
- const delay = baseBackoff * Math.pow(2, attempt - 1);
12380
- options.logger.warn(`[${options.context}] HTTP ${res.status} (attempt ${attempt}/${maxAttempts}), ${delay}ms 后重试, body=${errText.slice(0, 200)}`);
12381
- await sleep2(delay);
12382
- continue;
12383
- }
12384
- return res;
12385
- } catch (err2) {
12386
- lastError = err2;
12387
- if (attempt < maxAttempts) {
12388
- const delay = baseBackoff * Math.pow(2, attempt - 1);
12389
- const msg = err2?.message ?? String(err2);
12390
- options.logger.warn(`[${options.context}] 网络异常 (attempt ${attempt}/${maxAttempts}): ${msg}, ${delay}ms 后重试`);
12391
- await sleep2(delay);
12392
- continue;
12393
- }
12394
- }
12395
- }
12396
- throw lastError instanceof Error ? lastError : new Error(String(lastError));
12397
- }
12398
- function formatTimestamp(ms) {
12399
- const totalSeconds = Math.floor(ms / 1000);
12400
- const minutes = Math.floor(totalSeconds / 60);
12401
- const seconds = totalSeconds % 60;
12402
- return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
12403
- }
12404
- function formatDuration(seconds) {
12405
- const h = Math.floor(seconds / 3600);
12406
- const m = Math.floor(seconds % 3600 / 60);
12407
- const s = Math.floor(seconds % 60);
12408
- if (h > 0) {
12409
- return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
12410
- }
12411
- return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
12412
- }
12413
- function hasText(value) {
12414
- return typeof value === "string" && value.trim().length > 0;
12415
- }
12416
- function formatTranscriptSegmentText(segment) {
12417
- const text = segment.text.trim();
12418
- if (!text) {
12419
- return text;
12420
- }
12421
- if (typeof segment.speaker_id === "number") {
12422
- return `说话人${segment.speaker_id}:${text}`;
12423
- }
12424
- return text;
12425
- }
12426
- // src/recording/whisper-local.ts
12427
- var MODEL_DISK_SIZES = {
12428
- tiny: 75 * 1024 * 1024,
12429
- base: 140 * 1024 * 1024,
12430
- small: 460 * 1024 * 1024,
12431
- medium: 1500 * 1024 * 1024,
12432
- "large-v3": 3000 * 1024 * 1024
12433
- };
12434
- // src/recording/handler.ts
12435
- function emitRecordingStatus(recordingId, storage2, logger, notifyStatus, error, extras) {
12436
- if (!notifyStatus) {
12437
- return;
12438
- }
12439
- const entry = storage2.findById(recordingId);
12440
- if (!entry) {
12441
- return;
12442
- }
12443
- const title = extras?.title?.trim() || entry.title?.trim() || entry.metadata.name?.trim() || entry.id;
12444
- const persistedError = entry.lastError?.trim() || undefined;
12445
- try {
12446
- notifyStatus({
12447
- recordingId: entry.id,
12448
- transfer_status: entry.status,
12449
- audioFile: entry.audioFile,
12450
- srtFile: entry.srtFile,
12451
- transcriptDataFile: entry.transcriptDataFile,
12452
- transcriptFile: entry.transcriptFile,
12453
- transcript: extras?.transcript,
12454
- summary: extras?.summary,
12455
- title,
12456
- updatedAt: entry.updatedAt,
12457
- error: error ?? persistedError
12458
- });
12459
- } catch (err2) {
12460
- logger.error(`[recording-status] 状态事件发送失败: ${recordingId}, ${err2?.message ?? err2}`);
12461
- }
12462
- }
12463
- async function runRecordingSyncInBackground(metadata, recordingId, storage2, asrConfig, logger, options) {
12464
- const audioDestPath = storage2.getAudioFilePath(recordingId, metadata.oss_audio_url);
12465
- const srtDestPath = storage2.getSrtFilePath(recordingId);
12466
- logger.info(`[recording-sync] 开始下载录音文件: ${recordingId}, audio=${metadata.oss_audio_url}`);
12467
- const downloadResult = await downloadRecordingFiles(metadata.oss_audio_url, metadata.oss_srt_url, audioDestPath, srtDestPath, logger);
12468
- if (!downloadResult.audio.ok) {
12469
- const error = `音频下载失败: ${downloadResult.audio.error}`;
12470
- logger.error(`[recording-sync] ${error}: ${recordingId}`);
12471
- storage2.updateStatus(recordingId, "sync_failed");
12472
- storage2.setLastError(recordingId, error);
12473
- emitRecordingStatus(recordingId, storage2, logger, options.notifyStatus, error);
12474
- return;
12475
- }
12476
- storage2.setLastError(recordingId, undefined);
12477
- storage2.setAudioFile(recordingId, storage2.buildAudioFilename(recordingId, metadata.oss_audio_url));
12478
- if (downloadResult.srt?.ok) {
12479
- storage2.setSrtFile(recordingId, storage2.buildSrtFilename(recordingId));
12480
- }
12481
- const currentBeforeSynced = storage2.findById(recordingId);
12482
- if (currentBeforeSynced?.status === "syncing_openclaw" || currentBeforeSynced?.status === "sync_failed") {
12483
- storage2.updateStatus(recordingId, "synced");
12484
- } else {
12485
- logger.info(`[recording-sync] 录音已被其他通道更新,保留状态: ${recordingId}, status=${currentBeforeSynced?.status}`);
12486
- }
12487
- logger.info(`[recording-sync] 录音已同步: ${recordingId}`);
12488
- emitRecordingStatus(recordingId, storage2, logger, options.notifyStatus);
12489
- const currentAfterSynced = storage2.findById(recordingId);
12490
- if (isAsrConfigured(asrConfig) && canStartTranscription(currentAfterSynced?.status ?? "synced")) {
12491
- await triggerTranscription(recordingId, storage2, asrConfig, logger, options);
12492
- }
12493
- }
12494
- async function handleRecordingSync(recordingId, metadata, storage2, asrConfig, logger, options = {}) {
12495
- const existing = storage2.findById(recordingId);
12496
- const shouldDownloadAndSync = !existing || existing.metadata.oss_audio_url !== metadata.oss_audio_url || !existing.audioFile || existing.status === "syncing_openclaw" || existing.status === "sync_failed";
12497
- storage2.ingest(recordingId, metadata);
12498
- if (shouldDownloadAndSync) {
12499
- runRecordingSyncInBackground(metadata, recordingId, storage2, asrConfig, logger, options).catch((err2) => {
12500
- const error = `录音同步失败: ${err2?.message ?? err2}`;
12501
- logger.error(`[recording-sync] ${error}: ${recordingId}`);
12502
- const current2 = storage2.findById(recordingId);
12503
- if (current2?.status === "syncing_openclaw") {
12504
- storage2.updateStatus(recordingId, "sync_failed");
12505
- }
12506
- storage2.setLastError(recordingId, error);
12507
- emitRecordingStatus(recordingId, storage2, logger, options.notifyStatus, error);
12508
- });
12509
- } else {
12510
- logger.info(`[recording-sync] 录音已存在且音频未变化,跳过重复下载: ${recordingId}`);
12511
- emitRecordingStatus(recordingId, storage2, logger, options.notifyStatus);
12512
- const current2 = storage2.findById(recordingId);
12513
- if (current2?.status === "synced" && isAsrConfigured(asrConfig)) {
12514
- triggerTranscription(recordingId, storage2, asrConfig, logger, options).catch((err2) => {
12515
- logger.error(`[asr-trigger] 转写触发失败: ${recordingId}, ${err2?.message ?? err2}`);
12516
- });
12517
- }
12518
- }
12519
- const current = storage2.findById(recordingId);
12520
- return {
12521
- ok: true,
12522
- recordingId,
12523
- transfer_status: current?.status ?? "syncing_openclaw",
12524
- ...current?.lastError ? { error: current.lastError } : {}
12525
- };
12526
- }
12527
- async function triggerTranscription(recordingId, storage2, asrConfig, logger, options = {}) {
12528
- const entry = storage2.findById(recordingId);
12529
- if (!entry) {
12530
- logger.warn(`[asr-trigger] 录音不存在: ${recordingId}`);
12531
- return;
12532
- }
12533
- if (!canStartTranscription(entry.status)) {
12534
- logger.warn(`[asr-trigger] 当前状态不允许转写: ${recordingId} (status=${entry.status})`);
12535
- return;
12536
- }
12537
- storage2.updateStatus(recordingId, "transcribing");
12538
- storage2.setLastError(recordingId, undefined);
12539
- emitRecordingStatus(recordingId, storage2, logger, options.notifyStatus);
12540
- const audioFilePath = storage2.getAudioFilePath(recordingId);
12541
- let result;
12542
- try {
12543
- result = await runTranscriptionWorkflow({
12544
- audioFilePath,
12545
- audioOssUrl: entry.metadata.oss_audio_url,
12546
- config: asrConfig,
12547
- markers: entry.metadata.markers ?? [],
12548
- recordingName: entry.metadata.name,
12549
- durationSec: entry.metadata.duration_sec,
12550
- createdAt: entry.metadata.created_at,
12551
- transcriptDataDir: storage2.getTranscriptDataDir(),
12552
- transcriptsDir: storage2.getTranscriptsDir(),
12553
- summariesDir: storage2.getSummariesDir(),
12554
- recordingId,
12555
- logger
12556
- });
12557
- } catch (err2) {
12558
- const error = `转写任务异常: ${err2?.message ?? err2}`;
12559
- logger.error(`[asr-trigger] ${error}: ${recordingId}`);
12560
- result = { ok: false, error };
12561
- }
12562
- if (result.ok && result.transcriptFilename) {
12563
- if (result.transcriptDataFilename) {
12564
- storage2.setTranscriptDataFile(recordingId, result.transcriptDataFilename);
12565
- }
12566
- storage2.setTranscriptFile(recordingId, result.transcriptFilename);
12567
- if (result.summaryFilename) {
12568
- storage2.setSummaryFile(recordingId, result.summaryFilename);
12569
- }
12570
- storage2.setTitle(recordingId, result.title);
12571
- storage2.updateStatus(recordingId, "transcribed");
12572
- emitRecordingStatus(recordingId, storage2, logger, options.notifyStatus, undefined, {
12573
- transcript: result.transcript ?? [],
12574
- summary: result.summary,
12575
- title: result.title ?? ""
12576
- });
12577
- logger.info(`[asr-trigger] 转写完成: ${recordingId}, summary="${result.summary}"`);
12578
- } else {
12579
- const error = result.error ?? "ASR 转写失败";
12580
- storage2.updateStatus(recordingId, "transcribe_failed");
12581
- storage2.setLastError(recordingId, error);
12582
- emitRecordingStatus(recordingId, storage2, logger, options.notifyStatus, error);
12583
- logger.error(`[asr-trigger] 转写失败: ${recordingId}, error=${error}`);
12584
- }
12585
- }
12586
- // src/recording/result-writer.ts
12587
- var import_node_fs29 = require("node:fs");
12588
- var import_node_path25 = require("node:path");
12589
- function handleRecordingResultWrite(params, storage2, logger, options = {}) {
12590
- const recordingId = normalizeRequiredText(params.recordingId, "recordingId");
12591
- if (!params.transcript && !params.summary) {
12592
- throw new Error("transcript or summary is required");
12593
- }
12594
- let entry = storage2.findById(recordingId);
12595
- if (!entry) {
12596
- storage2.ingest(recordingId, buildPlaceholderMetadata(recordingId, params));
12597
- entry = storage2.findById(recordingId);
12598
- if (!entry) {
12599
- throw new Error(`Recording not found: ${recordingId}`);
12600
- }
12601
- logger.info(`[recording-result] 录音不存在,已按结果写入新建: ${recordingId}`);
12602
- }
12603
- let transcript;
12604
- let title = entry.title ?? entry.metadata.name ?? recordingId;
12605
- let summaryText;
12606
- if (params.transcript) {
12607
- const written = writeTranscript(recordingId, entry.metadata, params.transcript, storage2, logger);
12608
- transcript = written.transcript;
12609
- title = written.title;
12610
- }
12611
- if (params.summary) {
12612
- summaryText = writeSummary(recordingId, params.summary, storage2, logger);
12613
- }
12614
- const updated = storage2.markResultWritten(recordingId);
12615
- emitResultStatus(recordingId, storage2, logger, options.notifyStatus, {
12616
- transcript,
12617
- summary: summaryText ?? storage2.readSummary(recordingId),
12618
- title
12619
- });
12620
- const ossUrl = normalizeText(params.ossUrl);
12621
- if (ossUrl) {
12622
- downloadAudioInBackground(recordingId, ossUrl, storage2, logger, options);
12623
- }
12624
- return {
12625
- ok: true,
12626
- recordingId,
12627
- transfer_status: updated.status,
12628
- stored: {
12629
- transcriptDataFile: updated.transcriptDataFile,
12630
- transcriptFile: updated.transcriptFile,
12631
- summaryFile: updated.summaryFile
12632
- }
12633
- };
12634
- }
12635
- function buildPlaceholderMetadata(recordingId, params) {
12636
- return {
12637
- name: normalizeText(params.transcript?.title) ?? recordingId,
12638
- duration_sec: 0,
12639
- file_size_bytes: 0,
12640
- created_at: normalizeText(params.transcript?.generatedAt) ?? new Date().toISOString(),
12641
- oss_audio_url: normalizeText(params.ossUrl) ?? "",
12642
- markers: [],
12643
- transfer_status: "synced"
12644
- };
12645
- }
12646
- function writeTranscript(recordingId, recording, transcript, storage2, logger) {
12647
- const title = normalizeText(transcript.title) ?? normalizeText(recording.name) ?? recordingId;
12648
- const segments = normalizeSegments2(transcript.segments);
12649
- const text = normalizePossiblyEmptyText2(transcript.text) ?? joinSegmentTexts(segments) ?? normalizePossiblyEmptyText2(transcript.markdown) ?? "";
12650
- const transcriptDocument = buildTranscriptDocument({
12651
- recordingId,
12652
- generatedAt: normalizeText(transcript.generatedAt) ?? new Date().toISOString(),
12653
- source: {
12654
- provider: normalizeText(transcript.source?.provider) ?? "app",
12655
- taskId: normalizeText(transcript.source?.taskId),
12656
- requestId: normalizeText(transcript.source?.requestId),
12657
- status: normalizeText(transcript.source?.status),
12658
- delivery: "result-write"
12659
- },
12660
- title,
12661
- category: normalizeText(transcript.category),
12662
- summary: normalizePossiblyEmptyText2(transcript.brief),
12663
- text,
12664
- segments,
12665
- raw: transcript
12666
- });
12667
- const transcriptDataFilename = storage2.buildTranscriptDataFilename(recordingId);
12668
- import_node_fs29.writeFileSync(storage2.getTranscriptDataFilePath(recordingId), JSON.stringify(transcriptDocument, null, 2), "utf-8");
12669
- storage2.setTranscriptDataFile(recordingId, transcriptDataFilename);
12670
- const transcriptFilename = storage2.buildTranscriptFilename(recordingId, title);
12671
- const markdown = normalizePossiblyEmptyText2(transcript.markdown) ?? buildTranscriptMarkdown2(recording, title, text, segments);
12672
- import_node_fs29.writeFileSync(import_node_path25.join(storage2.getTranscriptsDir(), transcriptFilename), ensureTrailingNewline(markdown), "utf-8");
12673
- storage2.setTranscriptFile(recordingId, transcriptFilename);
12674
- storage2.setTitle(recordingId, title);
12675
- logger.info(`[recording-result] 转录文本已写入: ${recordingId}`);
12676
- return {
12677
- title,
12678
- transcript: extractSourceTextListFromDocument(transcriptDocument) ?? []
12679
- };
12680
- }
12681
- function writeSummary(recordingId, summary, storage2, logger) {
12682
- const markdown = normalizePossiblyEmptyText2(summary.markdown) ?? buildStructuredSummaryMarkdown(summary.structured);
12683
- if (markdown === undefined) {
12684
- return;
12685
- }
12686
- const summaryFilename = storage2.buildSummaryFilename(recordingId);
12687
- import_node_fs29.writeFileSync(storage2.getSummaryFilePath(recordingId), ensureTrailingNewline(markdown), "utf-8");
12688
- storage2.setSummaryFile(recordingId, summaryFilename);
12689
- logger.info(`[recording-result] 总结已写入: ${recordingId}`);
12690
- return markdown.trim();
12691
- }
12692
- function downloadAudioInBackground(recordingId, ossUrl, storage2, logger, options) {
12693
- const audioDestPath = storage2.getAudioFilePath(recordingId, ossUrl);
12694
- logger.info(`[recording-result] 开始下载音频: ${recordingId}, audio=${ossUrl}`);
12695
- downloadFile(ossUrl, audioDestPath, logger).then((result) => {
12696
- if (!result.ok) {
12697
- const error = `音频下载失败: ${result.error}`;
12698
- logger.error(`[recording-result] ${error}: ${recordingId}`);
12699
- storage2.setLastError(recordingId, error);
12700
- emitDownloadStatus(recordingId, storage2, logger, options.notifyStatus);
12701
- return;
12702
- }
12703
- storage2.setAudioFile(recordingId, storage2.buildAudioFilename(recordingId, ossUrl));
12704
- logger.info(`[recording-result] 音频已更新: ${recordingId}`);
12705
- emitDownloadStatus(recordingId, storage2, logger, options.notifyStatus);
12706
- }).catch((err2) => {
12707
- const error = `音频下载异常: ${err2?.message ?? err2}`;
12708
- logger.error(`[recording-result] ${error}: ${recordingId}`);
12709
- storage2.setLastError(recordingId, error);
12710
- emitDownloadStatus(recordingId, storage2, logger, options.notifyStatus);
12711
- });
12712
- }
12713
- function emitDownloadStatus(recordingId, storage2, logger, notifyStatus) {
12714
- const entry = storage2.findById(recordingId);
12715
- if (!entry)
12716
- return;
12717
- emitResultStatus(recordingId, storage2, logger, notifyStatus, {
12718
- summary: storage2.readSummary(recordingId),
12719
- title: entry.title ?? entry.metadata.name ?? recordingId
12720
- });
12721
- }
12722
- function emitResultStatus(recordingId, storage2, logger, notifyStatus, extras) {
12723
- if (!notifyStatus)
12724
- return;
12725
- const entry = storage2.findById(recordingId);
12726
- if (!entry)
12727
- return;
12728
- try {
12729
- notifyStatus({
12730
- recordingId: entry.id,
12731
- transfer_status: entry.status,
12732
- audioFile: entry.audioFile,
12733
- srtFile: entry.srtFile,
12734
- transcriptDataFile: entry.transcriptDataFile,
12735
- transcriptFile: entry.transcriptFile,
12736
- summaryFile: entry.summaryFile,
12737
- transcript: extras.transcript,
12738
- summary: extras.summary,
12739
- title: extras.title,
12740
- updatedAt: entry.updatedAt,
12741
- error: entry.lastError
12742
- });
12743
- } catch (err2) {
12744
- logger.error(`[recording-result] 状态事件发送失败: ${recordingId}, ${err2?.message ?? err2}`);
12745
- }
12746
- }
12747
- function normalizeSegments2(segments) {
12748
- if (!Array.isArray(segments))
12749
- return [];
12750
- return segments.flatMap((segment) => {
12751
- const text = normalizeText(segment.text);
12752
- if (!text)
12753
- return [];
12754
- return [{
12755
- text,
12756
- startMs: typeof segment.startMs === "number" ? segment.startMs : undefined,
12757
- endMs: typeof segment.endMs === "number" ? segment.endMs : undefined,
12758
- speakerId: Number.isInteger(segment.speakerId) ? segment.speakerId : undefined
12759
- }];
12760
- });
12761
- }
12762
- function joinSegmentTexts(segments) {
12763
- const text = segments.map((segment) => segment.text).filter(Boolean).join(`
12764
-
12765
- `).trim();
12766
- return text || undefined;
12767
- }
12768
- function buildTranscriptMarkdown2(recording, title, text, segments) {
11943
+ function buildTranscriptMarkdown(recording, title, text, segments) {
12769
11944
  const lines = [];
12770
11945
  lines.push(`# ${title}`);
12771
11946
  lines.push("");
12772
11947
  lines.push(`> 录音名称:${recording.name}`);
12773
- lines.push(`> 时长:${formatDuration2(recording.duration_sec)}`);
11948
+ lines.push(`> 时长:${formatDuration(recording.duration_sec)}`);
12774
11949
  lines.push(`> 创建时间:${recording.created_at}`);
12775
11950
  if ((recording.markers ?? []).length > 0) {
12776
11951
  lines.push(`> 关键点数:${recording.markers.length}`);
@@ -12780,7 +11955,7 @@ function buildTranscriptMarkdown2(recording, title, text, segments) {
12780
11955
  lines.push("");
12781
11956
  if (segments.length > 0) {
12782
11957
  for (const segment of segments) {
12783
- const prefix = segment.startMs !== undefined ? `[${formatTimestamp2(segment.startMs)}] ` : "";
11958
+ const prefix = segment.startMs !== undefined ? `[${formatTimestamp(segment.startMs)}] ` : "";
12784
11959
  lines.push(`${prefix}${segment.text}`.trim());
12785
11960
  lines.push("");
12786
11961
  }
@@ -12841,7 +12016,7 @@ function ensureTrailingNewline(value) {
12841
12016
  `) ? value : `${value}
12842
12017
  `;
12843
12018
  }
12844
- function formatDuration2(sec) {
12019
+ function formatDuration(sec) {
12845
12020
  const hours = Math.floor(sec / 3600);
12846
12021
  const minutes = Math.floor(sec % 3600 / 60);
12847
12022
  const seconds = Math.floor(sec % 60);
@@ -12850,15 +12025,15 @@ function formatDuration2(sec) {
12850
12025
  }
12851
12026
  return `${minutes}:${String(seconds).padStart(2, "0")}`;
12852
12027
  }
12853
- function formatTimestamp2(ms) {
12028
+ function formatTimestamp(ms) {
12854
12029
  const totalSec = Math.floor(ms / 1000);
12855
12030
  const minutes = Math.floor(totalSec / 60);
12856
12031
  const seconds = totalSec % 60;
12857
12032
  return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
12858
12033
  }
12859
12034
  // src/image/store.ts
12860
- var import_node_fs30 = require("node:fs");
12861
- var import_node_path26 = require("node:path");
12035
+ var import_node_fs29 = require("node:fs");
12036
+ var import_node_path25 = require("node:path");
12862
12037
  var IMAGES_DIR = "images";
12863
12038
  var FILES_DIR = "files";
12864
12039
  var INDEX_FILE2 = "index.json";
@@ -12873,16 +12048,16 @@ function extFromMime(mime) {
12873
12048
  return "jpg";
12874
12049
  }
12875
12050
  function resolveImageStorageDir(ctx, logger) {
12876
- const stateImagesDir = import_node_path26.join(ctx.stateDir, "plugins", "phone-notifications", IMAGES_DIR);
12051
+ const stateImagesDir = import_node_path25.join(ctx.stateDir, "plugins", "phone-notifications", IMAGES_DIR);
12877
12052
  try {
12878
- import_node_fs30.mkdirSync(stateImagesDir, { recursive: true });
12053
+ import_node_fs29.mkdirSync(stateImagesDir, { recursive: true });
12879
12054
  logger.info(`图片将写入 stateDir 路径: ${stateImagesDir}`);
12880
12055
  return stateImagesDir;
12881
12056
  } catch {}
12882
12057
  if (ctx.workspaceDir) {
12883
- const wsImagesDir = import_node_path26.join(ctx.workspaceDir, IMAGES_DIR);
12058
+ const wsImagesDir = import_node_path25.join(ctx.workspaceDir, IMAGES_DIR);
12884
12059
  try {
12885
- import_node_fs30.mkdirSync(wsImagesDir, { recursive: true });
12060
+ import_node_fs29.mkdirSync(wsImagesDir, { recursive: true });
12886
12061
  logger.warn(`stateDir 不可用,图片已回退到 workspace 路径: ${wsImagesDir}`);
12887
12062
  return wsImagesDir;
12888
12063
  } catch {}
@@ -12899,11 +12074,11 @@ class ImageStorage {
12899
12074
  constructor(dir, logger) {
12900
12075
  this.logger = logger;
12901
12076
  this.dir = dir;
12902
- this.filesDir = import_node_path26.join(dir, FILES_DIR);
12903
- this.indexPath = import_node_path26.join(dir, INDEX_FILE2);
12077
+ this.filesDir = import_node_path25.join(dir, FILES_DIR);
12078
+ this.indexPath = import_node_path25.join(dir, INDEX_FILE2);
12904
12079
  }
12905
12080
  async init() {
12906
- import_node_fs30.mkdirSync(this.filesDir, { recursive: true });
12081
+ import_node_fs29.mkdirSync(this.filesDir, { recursive: true });
12907
12082
  this.loadIndex();
12908
12083
  this.logger.info(`图片存储已初始化: ${this.dir}(共 ${this.index.images.length} 条记录)`);
12909
12084
  }
@@ -12921,7 +12096,7 @@ class ImageStorage {
12921
12096
  return existing;
12922
12097
  }
12923
12098
  if (existing.localFile) {
12924
- import_node_fs30.rmSync(import_node_path26.join(this.dir, existing.localFile), { force: true });
12099
+ import_node_fs29.rmSync(import_node_path25.join(this.dir, existing.localFile), { force: true });
12925
12100
  }
12926
12101
  existing.metadata = metadata;
12927
12102
  existing.clientLabel = clientLabel;
@@ -12970,10 +12145,10 @@ class ImageStorage {
12970
12145
  return `${FILES_DIR}/${this.buildFilename(imageId, mime)}`;
12971
12146
  }
12972
12147
  getFilePath(imageId, mime) {
12973
- return import_node_path26.join(this.filesDir, this.buildFilename(imageId, mime));
12148
+ return import_node_path25.join(this.filesDir, this.buildFilename(imageId, mime));
12974
12149
  }
12975
12150
  resolveFile(relative) {
12976
- return import_node_path26.isAbsolute(relative) ? relative : import_node_path26.join(this.dir, relative);
12151
+ return import_node_path25.isAbsolute(relative) ? relative : import_node_path25.join(this.dir, relative);
12977
12152
  }
12978
12153
  findById(id) {
12979
12154
  return this.index.images.find((e) => e.imageId === id);
@@ -12982,12 +12157,12 @@ class ImageStorage {
12982
12157
  return [...this.index.images].sort((a, b) => b.metadata.created_at.localeCompare(a.metadata.created_at));
12983
12158
  }
12984
12159
  loadIndex() {
12985
- if (!import_node_fs30.existsSync(this.indexPath)) {
12160
+ if (!import_node_fs29.existsSync(this.indexPath)) {
12986
12161
  this.index = { images: [] };
12987
12162
  return;
12988
12163
  }
12989
12164
  try {
12990
- const raw = JSON.parse(import_node_fs30.readFileSync(this.indexPath, "utf-8"));
12165
+ const raw = JSON.parse(import_node_fs29.readFileSync(this.indexPath, "utf-8"));
12991
12166
  this.index = raw && Array.isArray(raw.images) ? { images: raw.images } : { images: [] };
12992
12167
  } catch {
12993
12168
  this.logger.warn(`图片索引文件损坏,已重建: ${this.indexPath}`);
@@ -12995,13 +12170,13 @@ class ImageStorage {
12995
12170
  }
12996
12171
  }
12997
12172
  saveIndex() {
12998
- import_node_fs30.writeFileSync(this.indexPath, JSON.stringify(this.index, null, 2), "utf-8");
12173
+ import_node_fs29.writeFileSync(this.indexPath, JSON.stringify(this.index, null, 2), "utf-8");
12999
12174
  }
13000
12175
  async close() {}
13001
12176
  }
13002
12177
  // src/image/handler.ts
13003
- var import_node_fs31 = require("node:fs");
13004
- var import_node_path27 = require("node:path");
12178
+ var import_node_fs30 = require("node:fs");
12179
+ var import_node_path26 = require("node:path");
13005
12180
  var DEFAULT_IMAGE_MAX_BYTES = 20 * 1024 * 1024;
13006
12181
  var DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1000;
13007
12182
  function handleImageSync(imageId, metadata, storage2, maxBytes, logger) {
@@ -13034,13 +12209,13 @@ async function runImageDownloadInBackground(imageId, metadata, storage2, maxByte
13034
12209
  logger.info(`[image-sync] 图片已同步: ${imageId} (${result.sizeBytes} bytes)`);
13035
12210
  }
13036
12211
  async function downloadImage(url, destPath, maxBytes, logger) {
13037
- import_node_fs31.mkdirSync(import_node_path27.dirname(destPath), { recursive: true });
12212
+ import_node_fs30.mkdirSync(import_node_path26.dirname(destPath), { recursive: true });
13038
12213
  const controller = new AbortController;
13039
12214
  const timer = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS);
13040
12215
  const cleanup = () => {
13041
12216
  try {
13042
- if (import_node_fs31.existsSync(destPath))
13043
- import_node_fs31.unlinkSync(destPath);
12217
+ if (import_node_fs30.existsSync(destPath))
12218
+ import_node_fs30.unlinkSync(destPath);
13044
12219
  } catch {}
13045
12220
  };
13046
12221
  try {
@@ -13055,7 +12230,7 @@ async function downloadImage(url, destPath, maxBytes, logger) {
13055
12230
  if (contentLength > 0 && contentLength > maxBytes) {
13056
12231
  return { ok: false, error: `图片超过上限 ${maxBytes} 字节` };
13057
12232
  }
13058
- const writeStream = import_node_fs31.createWriteStream(destPath);
12233
+ const writeStream = import_node_fs30.createWriteStream(destPath);
13059
12234
  let total = 0;
13060
12235
  try {
13061
12236
  for await (const chunk of res.body) {
@@ -13089,12 +12264,12 @@ async function downloadImage(url, destPath, maxBytes, logger) {
13089
12264
  }
13090
12265
  }
13091
12266
  // src/tunnel/service.ts
13092
- var import_node_fs33 = require("node:fs");
13093
- var import_node_path30 = require("node:path");
12267
+ var import_node_fs32 = require("node:fs");
12268
+ var import_node_path29 = require("node:path");
13094
12269
 
13095
12270
  // src/tunnel/relay-client.ts
13096
- var import_node_fs32 = require("node:fs");
13097
- var import_node_path28 = require("node:path");
12271
+ var import_node_fs31 = require("node:fs");
12272
+ var import_node_path27 = require("node:path");
13098
12273
 
13099
12274
  // node_modules/ws/wrapper.mjs
13100
12275
  var import_stream = __toESM(require_stream(), 1);
@@ -13182,6 +12357,7 @@ function maskableSecret(credential) {
13182
12357
  var HANDSHAKE_TIMEOUT_MS = 15000;
13183
12358
  var CONNECT_WATCHDOG_MS = 20000;
13184
12359
  var SEND_SKIPPED_LOG_INTERVAL_MS = 30000;
12360
+ var WS_TRY_AGAIN_LATER_CODE = 1013;
13185
12361
 
13186
12362
  class RelayClient {
13187
12363
  opts;
@@ -13210,8 +12386,8 @@ class RelayClient {
13210
12386
  lastDisconnectReason
13211
12387
  };
13212
12388
  try {
13213
- import_node_fs32.mkdirSync(import_node_path28.dirname(this.opts.statusFilePath), { recursive: true });
13214
- import_node_fs32.writeFileSync(this.opts.statusFilePath, JSON.stringify(info, null, 2));
12389
+ import_node_fs31.mkdirSync(import_node_path27.dirname(this.opts.statusFilePath), { recursive: true });
12390
+ import_node_fs31.writeFileSync(this.opts.statusFilePath, JSON.stringify(info, null, 2));
13215
12391
  } catch {}
13216
12392
  }
13217
12393
  onInbound(handler) {
@@ -13434,7 +12610,9 @@ class RelayClient {
13434
12610
  const disconnectReason = `code=${code}, reason=${reasonStr}`;
13435
12611
  this.writeStatus("disconnected", disconnectReason);
13436
12612
  this.emitDisconnected(disconnectReason);
13437
- this.scheduleReconnect();
12613
+ this.scheduleReconnect({
12614
+ immediate: code === WS_TRY_AGAIN_LATER_CODE
12615
+ });
13438
12616
  }
13439
12617
  settle();
13440
12618
  });
@@ -13585,16 +12763,16 @@ class RelayClient {
13585
12763
  this.skippedSendLogLastAt = now;
13586
12764
  this.skippedSendLogSuppressed = 0;
13587
12765
  }
13588
- scheduleReconnect() {
12766
+ scheduleReconnect(opts = {}) {
13589
12767
  if (this.aborted)
13590
12768
  return;
13591
12769
  if (this.reconnectTimer) {
13592
12770
  return;
13593
12771
  }
13594
12772
  const baseMs = this.opts.reconnectBackoffMs;
13595
- const delayMs = Math.min(baseMs * Math.pow(2, this.reconnectAttempt), 60000);
12773
+ const delayMs = opts.immediate ? 0 : Math.min(baseMs * Math.pow(2, this.reconnectAttempt), 60000);
13596
12774
  this.reconnectAttempt++;
13597
- this.opts.logger.info(`Relay tunnel: reconnecting in ${delayMs}ms (attempt ${this.reconnectAttempt})`);
12775
+ this.opts.logger.info(`Relay tunnel: reconnecting in ${delayMs}ms (attempt ${this.reconnectAttempt}${opts.immediate ? ", immediate" : ""})`);
13598
12776
  this.reconnectTimer = setTimeout(() => {
13599
12777
  this.reconnectTimer = null;
13600
12778
  if (!this.aborted) {
@@ -13611,13 +12789,13 @@ var import_node_crypto5 = require("node:crypto");
13611
12789
 
13612
12790
  // src/host-sdk.ts
13613
12791
  var import_node_module = require("node:module");
13614
- var import_node_path29 = require("node:path");
12792
+ var import_node_path28 = require("node:path");
13615
12793
  var import_node_url = require("node:url");
13616
12794
  function resolveHostSdkModuleFromHostEntry(specifier, hostEntry = process.argv[1]) {
13617
12795
  if (!hostEntry?.trim())
13618
12796
  return "";
13619
12797
  try {
13620
- return import_node_module.createRequire(import_node_path29.resolve(hostEntry)).resolve(specifier);
12798
+ return import_node_module.createRequire(import_node_path28.resolve(hostEntry)).resolve(specifier);
13621
12799
  } catch {
13622
12800
  return "";
13623
12801
  }
@@ -14137,11 +13315,40 @@ class WsProxy {
14137
13315
  }
14138
13316
 
14139
13317
  // src/tunnel/frame-slimmer.ts
14140
- var SLIM_TRIGGER_CHARS = 32 * 1024;
13318
+ var SLIM_TRIGGER_CHARS = 24 * 1024;
14141
13319
  var AGENT_OUTPUT_CAP = 16 * 1024;
14142
13320
  var SESSION_LIST_PREVIEW_CAP = 256;
14143
- var SESSION_LIST_MESSAGE_CAP = 1024;
13321
+ var SESSION_LIST_MESSAGE_CAP = 256;
13322
+ var SESSION_LIST_ITEM_CAP = 50;
13323
+ var SESSION_LIST_SCALAR_CAP = 512;
13324
+ var SESSION_LIST_SMALL_VALUE_CAP = 1024;
13325
+ var CRON_RUN_TEXT_CAP = 1024;
13326
+ var CONFIG_TEXT_CAP = 1024;
13327
+ var CONFIG_ARRAY_CAP = 50;
14144
13328
  var COMMAND_SUMMARY_CAP = 4 * 1024;
13329
+ var CONFIG_RAW_PLACEHOLDER = "[omitted by relay tunnel from large config.get response]";
13330
+ function isRecord2(value) {
13331
+ return !!value && typeof value === "object" && !Array.isArray(value);
13332
+ }
13333
+ function isJsonScalar(value) {
13334
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
13335
+ }
13336
+ function jsonLength(value) {
13337
+ try {
13338
+ return JSON.stringify(value).length;
13339
+ } catch {
13340
+ return Number.POSITIVE_INFINITY;
13341
+ }
13342
+ }
13343
+ function slimScalar(value, cap, preview = false) {
13344
+ if (typeof value !== "string" || value.length <= cap) {
13345
+ return { value, changed: false };
13346
+ }
13347
+ return {
13348
+ value: preview ? truncatePreviewText(value, cap) : truncateText(value, cap),
13349
+ changed: true
13350
+ };
13351
+ }
14145
13352
  function safeSliceEnd(text, end) {
14146
13353
  const code = text.charCodeAt(end - 1);
14147
13354
  return code >= 55296 && code <= 56319 ? end - 1 : end;
@@ -14200,41 +13407,89 @@ function slimToolResultContentItem(item) {
14200
13407
  }
14201
13408
  return changed;
14202
13409
  }
14203
- function slimStringProp(target, prop, cap, preview = false) {
14204
- if (!target || typeof target !== "object")
14205
- return false;
14206
- if (typeof target[prop] !== "string" || target[prop].length <= cap) {
14207
- return false;
13410
+ function slimSessionMessageValue(value, cap) {
13411
+ if (isJsonScalar(value)) {
13412
+ return slimScalar(value, cap);
14208
13413
  }
14209
- target[prop] = preview ? truncatePreviewText(target[prop], cap) : truncateText(target[prop], cap);
14210
- return true;
14211
- }
14212
- function slimMessageTextContent(value, cap) {
14213
- if (!value)
14214
- return false;
14215
- if (typeof value === "object" && !Array.isArray(value)) {
14216
- let changed = false;
14217
- if (slimStringProp(value, "text", cap))
14218
- changed = true;
14219
- if (slimStringProp(value, "content", cap))
14220
- changed = true;
14221
- if (Array.isArray(value.content)) {
14222
- for (const item of value.content) {
14223
- if (slimMessageTextContent(item, cap))
14224
- changed = true;
14225
- }
13414
+ if (Array.isArray(value)) {
13415
+ const keepCount = Math.min(value.length, 6);
13416
+ const items = [];
13417
+ let changed2 = value.length > keepCount;
13418
+ for (let i = 0;i < keepCount; i++) {
13419
+ const item = slimSessionMessageValue(value[i], cap);
13420
+ items.push(item.value);
13421
+ if (item.changed)
13422
+ changed2 = true;
13423
+ }
13424
+ return { value: items, changed: changed2 };
13425
+ }
13426
+ if (!isRecord2(value))
13427
+ return { value, changed: false };
13428
+ const slimmed = {};
13429
+ let changed = false;
13430
+ for (const [key, child] of Object.entries(value)) {
13431
+ if (key === "content" || key === "text" || key === "summary") {
13432
+ const item = slimSessionMessageValue(child, cap);
13433
+ slimmed[key] = item.value;
13434
+ if (item.changed)
13435
+ changed = true;
13436
+ continue;
14226
13437
  }
14227
- return changed;
13438
+ if (isJsonScalar(child)) {
13439
+ const item = slimScalar(child, SESSION_LIST_SCALAR_CAP, true);
13440
+ slimmed[key] = item.value;
13441
+ if (item.changed)
13442
+ changed = true;
13443
+ continue;
13444
+ }
13445
+ if (jsonLength(child) <= SESSION_LIST_SMALL_VALUE_CAP) {
13446
+ slimmed[key] = child;
13447
+ continue;
13448
+ }
13449
+ changed = true;
14228
13450
  }
14229
- if (Array.isArray(value)) {
14230
- let changed = false;
14231
- for (const item of value) {
14232
- if (slimMessageTextContent(item, cap))
13451
+ if (Object.keys(slimmed).length !== Object.keys(value).length) {
13452
+ changed = true;
13453
+ }
13454
+ return { value: slimmed, changed };
13455
+ }
13456
+ function slimSessionListItem(session) {
13457
+ if (!isRecord2(session))
13458
+ return { value: session, changed: false };
13459
+ const slimmed = {};
13460
+ let changed = false;
13461
+ for (const [key, value] of Object.entries(session)) {
13462
+ if (key === "lastMessagePreview") {
13463
+ const item = slimScalar(value, SESSION_LIST_PREVIEW_CAP, true);
13464
+ slimmed[key] = item.value;
13465
+ if (item.changed)
14233
13466
  changed = true;
13467
+ continue;
14234
13468
  }
14235
- return changed;
13469
+ if (key === "lastMessage") {
13470
+ const item = slimSessionMessageValue(value, SESSION_LIST_MESSAGE_CAP);
13471
+ slimmed[key] = item.value;
13472
+ if (item.changed)
13473
+ changed = true;
13474
+ continue;
13475
+ }
13476
+ if (isJsonScalar(value)) {
13477
+ const item = slimScalar(value, SESSION_LIST_SCALAR_CAP, true);
13478
+ slimmed[key] = item.value;
13479
+ if (item.changed)
13480
+ changed = true;
13481
+ continue;
13482
+ }
13483
+ if (jsonLength(value) <= SESSION_LIST_SMALL_VALUE_CAP) {
13484
+ slimmed[key] = value;
13485
+ continue;
13486
+ }
13487
+ changed = true;
14236
13488
  }
14237
- return false;
13489
+ if (Object.keys(slimmed).length !== Object.keys(session).length) {
13490
+ changed = true;
13491
+ }
13492
+ return changed ? { value: slimmed, changed } : { value: session, changed };
14238
13493
  }
14239
13494
  function slimMessagesPayload(payload) {
14240
13495
  if (!Array.isArray(payload?.messages))
@@ -14269,18 +13524,93 @@ function slimSessionsListPayload(payload) {
14269
13524
  if (!Array.isArray(payload?.sessions))
14270
13525
  return false;
14271
13526
  let changed = false;
14272
- for (const session of payload.sessions) {
13527
+ if (payload.sessions.length > SESSION_LIST_ITEM_CAP) {
13528
+ payload.sessions = payload.sessions.slice(0, SESSION_LIST_ITEM_CAP);
13529
+ changed = true;
13530
+ }
13531
+ for (let i = 0;i < payload.sessions.length; i++) {
13532
+ const session = payload.sessions[i];
14273
13533
  if (!session || typeof session !== "object")
14274
13534
  continue;
14275
- if (slimStringProp(session, "lastMessagePreview", SESSION_LIST_PREVIEW_CAP, true)) {
13535
+ const item = slimSessionListItem(session);
13536
+ if (item.changed) {
13537
+ payload.sessions[i] = item.value;
13538
+ changed = true;
13539
+ }
13540
+ }
13541
+ return changed;
13542
+ }
13543
+ function slimNestedLargeValues(value, options, depth = 0) {
13544
+ if (!value || typeof value !== "object" || depth > options.maxDepth) {
13545
+ return false;
13546
+ }
13547
+ let changed = false;
13548
+ if (Array.isArray(value)) {
13549
+ if (typeof options.arrayCap === "number" && value.length > options.arrayCap) {
13550
+ value.splice(options.arrayCap);
13551
+ changed = true;
13552
+ }
13553
+ for (const item of value) {
13554
+ if (slimNestedLargeValues(item, options, depth + 1))
13555
+ changed = true;
13556
+ }
13557
+ return changed;
13558
+ }
13559
+ for (const [key, child] of Object.entries(value)) {
13560
+ if (key === "raw" && options.rawPlaceholder) {
13561
+ if (value[key] !== options.rawPlaceholder) {
13562
+ value[key] = options.rawPlaceholder;
13563
+ changed = true;
13564
+ }
13565
+ continue;
13566
+ }
13567
+ if (typeof child === "string" && child.length > options.textCap) {
13568
+ value[key] = truncateText(child, options.textCap);
14276
13569
  changed = true;
13570
+ continue;
13571
+ }
13572
+ if (child && typeof child === "object") {
13573
+ if (slimNestedLargeValues(child, options, depth + 1))
13574
+ changed = true;
13575
+ }
13576
+ }
13577
+ return changed;
13578
+ }
13579
+ function slimCronRunsPayload(payload) {
13580
+ if (!Array.isArray(payload?.entries))
13581
+ return false;
13582
+ let hasCronRun = false;
13583
+ for (const entry of payload.entries) {
13584
+ if (isRecord2(entry) && (("jobId" in entry) || ("action" in entry) && ("status" in entry))) {
13585
+ hasCronRun = true;
13586
+ break;
14277
13587
  }
14278
- if (slimMessageTextContent(session.lastMessage, SESSION_LIST_MESSAGE_CAP)) {
13588
+ }
13589
+ if (!hasCronRun)
13590
+ return false;
13591
+ let changed = false;
13592
+ for (const entry of payload.entries) {
13593
+ if (slimNestedLargeValues(entry, {
13594
+ textCap: CRON_RUN_TEXT_CAP,
13595
+ arrayCap: 20,
13596
+ maxDepth: 6
13597
+ })) {
14279
13598
  changed = true;
14280
13599
  }
14281
13600
  }
14282
13601
  return changed;
14283
13602
  }
13603
+ function slimConfigGetPayload(payload) {
13604
+ if (!isRecord2(payload) || !("parsed" in payload) || !("exists" in payload)) {
13605
+ return false;
13606
+ }
13607
+ return slimNestedLargeValues(payload, {
13608
+ textCap: CONFIG_TEXT_CAP,
13609
+ arrayCap: CONFIG_ARRAY_CAP,
13610
+ maxDepth: 8,
13611
+ rawPlaceholder: CONFIG_RAW_PLACEHOLDER
13612
+ });
13613
+ }
14284
13614
  function slimRelayFrame(frame) {
14285
13615
  if (frame?.type === "event" && frame.event === "agent") {
14286
13616
  return slimAgentEventPayload(frame.payload);
@@ -14288,7 +13618,9 @@ function slimRelayFrame(frame) {
14288
13618
  if (frame?.type === "res" && frame.ok === true) {
14289
13619
  const sessionsChanged = slimSessionsListPayload(frame.payload);
14290
13620
  const messagesChanged = slimMessagesPayload(frame.payload);
14291
- return sessionsChanged || messagesChanged;
13621
+ const cronChanged = slimCronRunsPayload(frame.payload);
13622
+ const configChanged = slimConfigGetPayload(frame.payload);
13623
+ return sessionsChanged || messagesChanged || cronChanged || configChanged;
14292
13624
  }
14293
13625
  return false;
14294
13626
  }
@@ -15073,7 +14405,7 @@ function createTunnelService(opts) {
15073
14405
  }
15074
14406
  function readLockInfo(filePath) {
15075
14407
  try {
15076
- const parsed = JSON.parse(import_node_fs33.readFileSync(filePath, "utf-8"));
14408
+ const parsed = JSON.parse(import_node_fs32.readFileSync(filePath, "utf-8"));
15077
14409
  const pid = typeof parsed.pid === "number" ? parsed.pid : null;
15078
14410
  const updatedAtMs = typeof parsed.updatedAt === "string" ? Date.parse(parsed.updatedAt) : Number.NaN;
15079
14411
  return {
@@ -15106,7 +14438,7 @@ function createTunnelService(opts) {
15106
14438
  if (!filePath)
15107
14439
  return;
15108
14440
  try {
15109
- import_node_fs33.writeFileSync(filePath, buildLockPayload(), { mode: 384 });
14441
+ import_node_fs32.writeFileSync(filePath, buildLockPayload(), { mode: 384 });
15110
14442
  } catch {}
15111
14443
  }, intervalMs);
15112
14444
  lockRefreshTimer.unref?.();
@@ -15120,25 +14452,25 @@ function createTunnelService(opts) {
15120
14452
  lockStartedAt = null;
15121
14453
  if (fd !== null) {
15122
14454
  try {
15123
- import_node_fs33.closeSync(fd);
14455
+ import_node_fs32.closeSync(fd);
15124
14456
  } catch {}
15125
14457
  }
15126
14458
  if (filePath) {
15127
14459
  try {
15128
- import_node_fs33.unlinkSync(filePath);
14460
+ import_node_fs32.unlinkSync(filePath);
15129
14461
  } catch {}
15130
14462
  }
15131
14463
  }
15132
14464
  function acquireLock(filePath) {
15133
- import_node_fs33.mkdirSync(import_node_path30.dirname(filePath), { recursive: true });
14465
+ import_node_fs32.mkdirSync(import_node_path29.dirname(filePath), { recursive: true });
15134
14466
  const heartbeatSec = opts.heartbeatSec ?? DEFAULT_HEARTBEAT_SEC;
15135
14467
  const refreshMs = heartbeatSec * 1000 * LOCK_REFRESH_HEARTBEAT_MULTIPLIER;
15136
14468
  const staleMs = heartbeatSec * 1000 * LOCK_STALE_HEARTBEAT_MULTIPLIER;
15137
14469
  for (let attempt = 0;attempt < 2; attempt++) {
15138
14470
  try {
15139
14471
  lockStartedAt = new Date().toISOString();
15140
- const fd = import_node_fs33.openSync(filePath, "wx", 384);
15141
- import_node_fs33.writeFileSync(fd, buildLockPayload());
14472
+ const fd = import_node_fs32.openSync(filePath, "wx", 384);
14473
+ import_node_fs32.writeFileSync(fd, buildLockPayload());
15142
14474
  lockFilePath = filePath;
15143
14475
  lockFd = fd;
15144
14476
  startLockRefresh(refreshMs);
@@ -15156,7 +14488,7 @@ function createTunnelService(opts) {
15156
14488
  const staleReason = describeStaleLock(ownerPid, updatedAt, staleMs);
15157
14489
  opts.logger.warn(`Relay tunnel: removing stale local lock (${staleReason})`);
15158
14490
  try {
15159
- import_node_fs33.unlinkSync(filePath);
14491
+ import_node_fs32.unlinkSync(filePath);
15160
14492
  } catch {}
15161
14493
  continue;
15162
14494
  }
@@ -15261,10 +14593,10 @@ function createTunnelService(opts) {
15261
14593
  const { logger } = opts;
15262
14594
  const hostStateDir = resolveHostStateDir(ctx.stateDir);
15263
14595
  const stateDir = resolveStateDir(hostStateDir);
15264
- const baseStateDir = import_node_path30.join(stateDir, "plugins", "phone-notifications");
14596
+ const baseStateDir = import_node_path29.join(stateDir, "plugins", "phone-notifications");
15265
14597
  logger.info(`Relay tunnel: starting (pid=${process.pid}, url=${redactUrlSecrets(opts.tunnelUrl)}, heartbeat=${opts.heartbeatSec ?? DEFAULT_HEARTBEAT_SEC}s, backoff=${opts.reconnectBackoffMs ?? DEFAULT_RECONNECT_BACKOFF_MS}ms, gateway=${opts.gatewayBaseUrl}, hasGatewayToken=${!!opts.gatewayToken}, hasGatewayPwd=${!!opts.gatewayPassword})`);
15266
- const statusFilePath = import_node_path30.join(baseStateDir, "tunnel-status.json");
15267
- const lockPath = import_node_path30.join(baseStateDir, "relay-tunnel.lock");
14598
+ const statusFilePath = import_node_path29.join(baseStateDir, "tunnel-status.json");
14599
+ const lockPath = import_node_path29.join(baseStateDir, "relay-tunnel.lock");
15268
14600
  if (!acquireLock(lockPath)) {
15269
14601
  return;
15270
14602
  }
@@ -15405,8 +14737,8 @@ class ArkclawAuthProvider extends BaseApiKeyAuthProvider {
15405
14737
  // src/profile/auth/jvsclaw.ts
15406
14738
  var import_node_crypto6 = require("node:crypto");
15407
14739
  var import_node_os3 = __toESM(require("node:os"));
15408
- var import_node_fs34 = require("node:fs");
15409
- var import_node_path31 = require("node:path");
14740
+ var import_node_fs33 = require("node:fs");
14741
+ var import_node_path30 = require("node:path");
15410
14742
  var CLAW_CODE = "JvsClaw";
15411
14743
  var RETRY_BASE_MS = 2000;
15412
14744
  var RETRY_MAX_MS = 60000;
@@ -15417,7 +14749,7 @@ var FATAL_READY_CODES = new Set([
15417
14749
  "54020",
15418
14750
  "54024"
15419
14751
  ]);
15420
- var JVSCLAW_LINK_SECRET_CONFIG_PATH = import_node_path31.join(DEFAULT_JVSCLAW_STATE_DIR, "openclaw.json");
14752
+ var JVSCLAW_LINK_SECRET_CONFIG_PATH = import_node_path30.join(DEFAULT_JVSCLAW_STATE_DIR, "openclaw.json");
15421
14753
  var PLUGIN_ID3 = "phone-notifications";
15422
14754
  var LINK_SECRET_WATCH_DEBOUNCE_MS = 250;
15423
14755
 
@@ -15472,7 +14804,7 @@ class JvsclawAuthProvider extends BaseApiKeyAuthProvider {
15472
14804
  }
15473
14805
  const delayMs = Math.min(RETRY_BASE_MS * 2 ** (attempt - 1), RETRY_MAX_MS);
15474
14806
  logger.warn(`jvsclaw auth: 置换失败(attempt ${attempt}): ${err2?.message ?? String(err2)},${delayMs}ms 后重试`);
15475
- const aborted = await sleep3(delayMs, abortSignal);
14807
+ const aborted = await sleep2(delayMs, abortSignal);
15476
14808
  if (aborted)
15477
14809
  break;
15478
14810
  }
@@ -15529,8 +14861,8 @@ class JvsclawAuthProvider extends BaseApiKeyAuthProvider {
15529
14861
  watch(onChange) {
15530
14862
  const unwatchCredentials = super.watch(onChange);
15531
14863
  const configPath = resolveConfigPath();
15532
- const configDir = import_node_path31.dirname(configPath);
15533
- const configFile = import_node_path31.basename(configPath);
14864
+ const configDir = import_node_path30.dirname(configPath);
14865
+ const configFile = import_node_path30.basename(configPath);
15534
14866
  let debounceTimer = null;
15535
14867
  let configWatcher = null;
15536
14868
  const emitChange = () => {
@@ -15543,7 +14875,7 @@ class JvsclawAuthProvider extends BaseApiKeyAuthProvider {
15543
14875
  debounceTimer.unref?.();
15544
14876
  };
15545
14877
  try {
15546
- configWatcher = import_node_fs34.watch(configDir, { persistent: false }, (_event, changedName) => {
14878
+ configWatcher = import_node_fs33.watch(configDir, { persistent: false }, (_event, changedName) => {
15547
14879
  if (!changedName)
15548
14880
  return;
15549
14881
  const name = String(changedName);
@@ -15568,7 +14900,7 @@ function hashLinkSecret(value) {
15568
14900
  function readJvsclawLinkSecretFromConfigFile(configPath = resolveConfigPath()) {
15569
14901
  let config;
15570
14902
  try {
15571
- config = JSON.parse(import_node_fs34.readFileSync(configPath, "utf-8"));
14903
+ config = JSON.parse(import_node_fs33.readFileSync(configPath, "utf-8"));
15572
14904
  } catch {
15573
14905
  return;
15574
14906
  }
@@ -15585,7 +14917,7 @@ function readObject2(value, key) {
15585
14917
  const nested = value[key];
15586
14918
  return nested && typeof nested === "object" ? nested : undefined;
15587
14919
  }
15588
- function sleep3(ms, signal) {
14920
+ function sleep2(ms, signal) {
15589
14921
  return new Promise((resolve4) => {
15590
14922
  if (signal?.aborted) {
15591
14923
  resolve4(true);
@@ -15682,7 +15014,7 @@ function readHostGatewayConfig(params) {
15682
15014
  let configData;
15683
15015
  if (configPath) {
15684
15016
  try {
15685
- configData = JSON.parse(import_node_fs35.readFileSync(configPath, "utf-8"));
15017
+ configData = JSON.parse(import_node_fs34.readFileSync(configPath, "utf-8"));
15686
15018
  } catch (err2) {
15687
15019
  if (err2?.code !== "ENOENT") {
15688
15020
  params.logger.warn(`Relay tunnel: 无法读取 gateway 鉴权配置 (${configPath})`);
@@ -16353,56 +15685,11 @@ function registerRecordingInterfaces(deps) {
16353
15685
  const {
16354
15686
  api,
16355
15687
  logger,
16356
- asrDataDir,
16357
15688
  getRecordingStorage,
16358
15689
  notifyRecordingStatus,
16359
15690
  registerGatewayMethod,
16360
- shouldBroadcastStatusOnHttp,
16361
15691
  tunnelService
16362
15692
  } = deps;
16363
- registerGatewayMethod("recordings.sync", async ({ params, respond }) => {
16364
- const recordingStorage = getRecordingStorage();
16365
- if (!recordingStorage) {
16366
- respond(false, null, {
16367
- code: "NOT_READY",
16368
- message: "Recording storage service not ready"
16369
- });
16370
- return;
16371
- }
16372
- const {
16373
- recordingId: rawRecordingId,
16374
- recording,
16375
- asr
16376
- } = params;
16377
- const recordingId = trimToUndefined2(rawRecordingId);
16378
- if (!recordingId) {
16379
- respond(false, null, {
16380
- code: "INVALID_PARAMS",
16381
- message: "recordingId is required"
16382
- });
16383
- return;
16384
- }
16385
- if (!recording || !recording.oss_audio_url || !recording.created_at) {
16386
- respond(false, null, {
16387
- code: "INVALID_PARAMS",
16388
- message: "recording with oss_audio_url and created_at is required"
16389
- });
16390
- return;
16391
- }
16392
- const asrError = asr ? validateAsrConfig(asr) : undefined;
16393
- if (asrError) {
16394
- respond(false, null, {
16395
- code: "INVALID_PARAMS",
16396
- message: asrError
16397
- });
16398
- return;
16399
- }
16400
- const result = await handleRecordingSync(recordingId, recording, recordingStorage, asr, logger, { notifyStatus: notifyRecordingStatus });
16401
- respond(result.ok, result, result.ok ? undefined : {
16402
- code: "SYNC_FAILED",
16403
- message: result.error ?? "Unknown error"
16404
- });
16405
- });
16406
15693
  registerGatewayMethod("recordings.result.write", async ({ params, respond }) => {
16407
15694
  const recordingStorage = getRecordingStorage();
16408
15695
  if (!recordingStorage) {
@@ -16570,69 +15857,6 @@ function registerRecordingInterfaces(deps) {
16570
15857
  message: `Recording not found: ${recordingId}`
16571
15858
  });
16572
15859
  });
16573
- registerGatewayMethod("recordings.retranscribe", async ({ params, respond }) => {
16574
- const recordingStorage = getRecordingStorage();
16575
- if (!recordingStorage) {
16576
- respond(false, null, {
16577
- code: "NOT_READY",
16578
- message: "Recording storage service not ready"
16579
- });
16580
- return;
16581
- }
16582
- const { recordingId, asr } = params;
16583
- if (!recordingId) {
16584
- respond(false, null, {
16585
- code: "INVALID_PARAMS",
16586
- message: "recordingId is required"
16587
- });
16588
- return;
16589
- }
16590
- const asrError = validateAsrConfig(asr);
16591
- if (asrError) {
16592
- respond(false, null, {
16593
- code: "ASR_NOT_CONFIGURED",
16594
- message: asrError
16595
- });
16596
- return;
16597
- }
16598
- const entry = recordingStorage.findById(recordingId);
16599
- if (!entry) {
16600
- respond(false, null, {
16601
- code: "NOT_FOUND",
16602
- message: `Recording not found: ${recordingId}`
16603
- });
16604
- return;
16605
- }
16606
- if (!canStartTranscription(entry.status)) {
16607
- respond(false, null, {
16608
- code: "INVALID_STATE",
16609
- message: `Recording status does not allow retranscribe: ${entry.status}`
16610
- });
16611
- return;
16612
- }
16613
- triggerTranscription(recordingId, recordingStorage, asr, logger, {
16614
- notifyStatus: notifyRecordingStatus
16615
- }).catch((err2) => {
16616
- logger.error(`[recordings.retranscribe] 重试转写失败: ${recordingId}, ${err2?.message ?? err2}`);
16617
- });
16618
- respond(true, { ok: true, recordingId, message: "转写已重新触发" });
16619
- });
16620
- registerGatewayMethod("recordings.asr.init", async ({ params, respond }) => {
16621
- const { asr } = params;
16622
- const asrError = validateAsrConfig(asr);
16623
- if (asrError) {
16624
- respond(false, null, {
16625
- code: "INVALID_PARAMS",
16626
- message: asrError
16627
- });
16628
- return;
16629
- }
16630
- const result = await initializeAsr(asr, asrDataDir, logger);
16631
- respond(result.ok, result, result.ok ? undefined : {
16632
- code: "ASR_INIT_FAILED",
16633
- message: result.error ?? "ASR init failed"
16634
- });
16635
- });
16636
15860
  api.registerHttpRoute({
16637
15861
  path: "/recordings",
16638
15862
  auth: "gateway",
@@ -16651,47 +15875,15 @@ function registerRecordingInterfaces(deps) {
16651
15875
  res.end(JSON.stringify({ ok: false, error: "Service Not Ready" }));
16652
15876
  return;
16653
15877
  }
16654
- let body;
16655
- try {
16656
- const raw = await readBody(req);
16657
- body = JSON.parse(raw);
16658
- } catch {
16659
- res.writeHead(400, { "Content-Type": "application/json" });
16660
- res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
16661
- return;
16662
- }
16663
- const recordingId = trimToUndefined2(body.recordingId);
16664
- if (!recordingId) {
16665
- res.writeHead(400, { "Content-Type": "application/json" });
16666
- res.end(JSON.stringify({
16667
- ok: false,
16668
- error: "recordingId is required"
16669
- }));
16670
- return;
16671
- }
16672
- if (!body.recording || !body.recording.oss_audio_url) {
16673
- res.writeHead(400, { "Content-Type": "application/json" });
16674
- res.end(JSON.stringify({
16675
- ok: false,
16676
- error: "recording with oss_audio_url is required"
16677
- }));
16678
- return;
16679
- }
16680
- const asrError = body.asr ? validateAsrConfig(body.asr) : undefined;
16681
- if (asrError) {
16682
- res.writeHead(400, { "Content-Type": "application/json" });
16683
- res.end(JSON.stringify({ ok: false, error: asrError }));
16684
- return;
16685
- }
16686
- const result = await handleRecordingSync(recordingId, body.recording, recordingStorage, body.asr, logger, shouldBroadcastStatusOnHttp() ? { notifyStatus: notifyRecordingStatus } : undefined);
16687
- res.writeHead(result.ok ? 200 : 500, {
16688
- "Content-Type": "application/json"
16689
- });
16690
- res.end(JSON.stringify(result));
15878
+ res.writeHead(410, { "Content-Type": "application/json" });
15879
+ res.end(JSON.stringify({
15880
+ ok: false,
15881
+ error: "POST /recordings 已停用,请改用 gateway 方法 recordings.result.write"
15882
+ }));
16691
15883
  }
16692
15884
  });
16693
- logger.info("Gateway 录音方法已注册: recordings.sync / recordings.result.write / recordings.list / recordings.status / recordings.rename / recordings.delete / recordings.retranscribe / recordings.asr.init");
16694
- logger.info("HTTP 录音端点已注册: POST /recordings");
15885
+ logger.info("Gateway 录音方法已注册: recordings.result.write / recordings.list / recordings.status / recordings.rename / recordings.delete");
15886
+ logger.info("HTTP 录音端点已注册: POST /recordings (已停用,返回 410)");
16695
15887
  }
16696
15888
 
16697
15889
  // src/plugin/images.ts
@@ -16858,9 +16050,8 @@ var src_default = {
16858
16050
  let tunnelService = null;
16859
16051
  const openclawDir = resolvePluginStateDir(api);
16860
16052
  const logger = openclawDir ? new PluginFileLogger(api.logger, openclawDir) : createVersionAwareLogger(api.logger);
16861
- const asrDataDir = openclawDir ?? resolveStateDir() ?? process.cwd();
16862
16053
  ensureNtfBinInstalled({
16863
- ntfCjsPath: import_node_path32.join(__dirname, "bin", "ntf.cjs"),
16054
+ ntfCjsPath: import_node_path31.join(__dirname, "bin", "ntf.cjs"),
16864
16055
  stateDir: openclawDir,
16865
16056
  logger
16866
16057
  });
@@ -16990,11 +16181,9 @@ var src_default = {
16990
16181
  registerRecordingInterfaces({
16991
16182
  api,
16992
16183
  logger,
16993
- asrDataDir,
16994
16184
  getRecordingStorage: () => recordingStorage,
16995
16185
  notifyRecordingStatus,
16996
16186
  registerGatewayMethod: registerGatewayMethodWithBroadcastCapture,
16997
- shouldBroadcastStatusOnHttp: () => !!broadcastFn,
16998
16187
  tunnelService
16999
16188
  });
17000
16189
  registerImageInterfaces({
@@ -17023,5 +16212,5 @@ var src_default = {
17023
16212
  }
17024
16213
  };
17025
16214
 
17026
- //# debugId=3838A06742EC9ED664756E2164756E21
16215
+ //# debugId=1C4AEF73472B01C364756E2164756E21
17027
16216
  //# sourceMappingURL=index.cjs.map