@nowcrew/daemon 0.6.86 → 0.6.87-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,10 +12,12 @@ npx @nowcrew/daemon@latest --server-url https://nowwork.example --api-key sk_mac
12
12
  ```
13
13
 
14
14
  The package has two release channels. The `in` environment consumes stable `x.y.z` releases through
15
- the `latest` tag; the `dev` environment consumes `x.y.z-beta.n` prereleases through the `beta` tag.
15
+ the `latest` tag. The `dev` environment prefers `x.y.z-beta.n` prereleases through the `beta` tag,
16
+ and also accepts stable `x.y.z` releases for compatibility and cross-channel updates.
16
17
  The parser also accepts the historical `x.y.z-beta-n` spelling and normalizes it for ordering; new
17
18
  published versions should use the dotted SemVer form.
18
- Never use `latest` for a dev installation or `beta` for an in installation. See the
19
+ Use `beta` by default for dev installations; an explicit `latest` target is also supported there.
20
+ Never use `beta` for an in installation. See the
19
21
  [Daemon Release Runbook](../docs/releases/daemon-release-runbook.md) for release and rollback procedures.
20
22
 
21
23
  This compatibility form stays resident in the current terminal and reconnects with backoff. New
@@ -78,6 +78,28 @@ const git = async (args, cwd) => {
78
78
  });
79
79
  return result.stdout.trim();
80
80
  };
81
+ export const parseAbilityRefLine = (line) => {
82
+ const [branch, commit, rawTime, ...extra] = line.split("\t");
83
+ const time = Number(rawTime);
84
+ if (!branch || !commit || extra.length > 0 || !Number.isSafeInteger(time)) {
85
+ throw new Error("ability_release_ref_metadata_invalid");
86
+ }
87
+ return { branch, commit, time };
88
+ };
89
+ export const selectAbilityRef = (lines, branchGlob, desiredCommit) => {
90
+ const refs = lines.filter(Boolean).map(parseAbilityRefLine)
91
+ .filter((candidate) => matchesAbilityBranch(branchGlob, candidate.branch));
92
+ if (refs.length === 0)
93
+ throw new Error("ability_release_branch_not_found");
94
+ const selected = refs.filter((candidate) => desiredCommit === undefined || candidate.commit === desiredCommit)
95
+ .sort((left, right) => right.time - left.time || right.branch.localeCompare(left.branch))[0];
96
+ if (!selected)
97
+ throw new Error("ability_desired_commit_not_found");
98
+ const committedDate = new Date(selected.time * 1_000);
99
+ return Number.isNaN(committedDate.getTime())
100
+ ? selected
101
+ : { ...selected, committedAt: committedDate.toISOString() };
102
+ };
81
103
  export const projectDependencyIds = (files, manifest) => {
82
104
  const path = manifest.spec.git?.projectDependencies.path;
83
105
  if (path === undefined)
@@ -209,17 +231,10 @@ const resolveRoot = async (cacheRoot, repositoryUrl, branchGlob, desiredCommit)
209
231
  else
210
232
  await git(["--git-dir", bare, "remote", "set-url", "origin", repositoryUrl]);
211
233
  await git(["--git-dir", bare, "fetch", "--no-tags", "--prune", "origin", "+refs/heads/*:refs/remotes/origin/*"]);
212
- const refs = (await git(["--git-dir", bare, "for-each-ref", "--format=%(refname:strip=3)\t%(objectname)\t%(committerdate:unix)", "refs/remotes/origin"])).split("\n")
213
- .filter(Boolean).map((line) => {
214
- const [branch, commit, time] = line.split("\t");
215
- return { branch: branch, commit: commit, time: Number(time) };
216
- }).filter((candidate) => matchesAbilityBranch(branchGlob, candidate.branch));
217
- if (refs.length === 0)
218
- throw new Error("ability_release_branch_not_found");
219
- const selected = refs.filter((candidate) => desiredCommit === undefined || candidate.commit === desiredCommit)
220
- .sort((left, right) => right.time - left.time || right.branch.localeCompare(left.branch))[0];
221
- if (!selected)
222
- throw new Error("ability_desired_commit_not_found");
234
+ const selected = selectAbilityRef((await git([
235
+ "--git-dir", bare, "for-each-ref",
236
+ "--format=%(refname:strip=3)\t%(objectname)\t%(committerdate:unix)", "refs/remotes/origin",
237
+ ])).split("\n"), branchGlob, desiredCommit);
223
238
  const root = join(cacheRoot, "artifacts", repositoryKey, selected.commit);
224
239
  try {
225
240
  await lstat(root);
@@ -440,7 +455,9 @@ export async function resolveAbilityRelease(agentsRoot, command) {
440
455
  }
441
456
  const projectDependencies = projectDependencyIds(files, manifest);
442
457
  return {
443
- branch: resolved.branch, rootCommit: resolved.commit, treeDigest: resolved.treeDigest,
458
+ branch: resolved.branch, rootCommit: resolved.commit,
459
+ ...(resolved.committedAt ? { committedAt: resolved.committedAt } : {}),
460
+ treeDigest: resolved.treeDigest,
444
461
  artifactDigest, schemaVersion: manifest.apiVersion, manifest, lock, assetSummary,
445
462
  scanSummary: { passed: security.findings.length === 0, blockingFindings: security.findings.length, findings: security.findings.slice(0, 100) },
446
463
  evalSummary: { passed: assertions.every((assertion) => assertion.passed), assertions },
@@ -27,9 +27,9 @@ export function assertReleaseTagForServerUrl(serverUrl, requested) {
27
27
  const known = KNOWN_TARGETS[parsed.hostname.toLowerCase()];
28
28
  if (known === undefined)
29
29
  return;
30
- const expected = known.environment === "dev" ? "beta" : "latest";
31
- if (requested !== expected) {
32
- throw new Error(`Server hostname '${parsed.hostname}' requires release tag '${expected}'`);
30
+ // dev is a compatibility lane: beta remains the default, but stable is also valid.
31
+ if (known.environment !== "dev" && requested !== "latest") {
32
+ throw new Error(`Server hostname '${parsed.hostname}' requires release tag 'latest'`);
33
33
  }
34
34
  }
35
35
  function parseServerUrl(value) {
@@ -88,7 +88,7 @@ function knownTargetForProfile(environment, requestedProfile, input, parsed) {
88
88
  daemonHome: local.daemonHome,
89
89
  agentsRoot: paths.join(local.root, environment === "production" ? "agents" : "agents-dev"),
90
90
  npmPrefix: local.runtimePrefix,
91
- releaseTag: environment === "dev" ? "beta" : "latest",
91
+ releaseTag: input.requestedReleaseTag ?? (environment === "dev" ? "beta" : "latest"),
92
92
  });
93
93
  }
94
94
  if (requestedProfile === "in" || requestedProfile === "dev") {
@@ -125,7 +125,7 @@ function knownTargetForProfile(environment, requestedProfile, input, parsed) {
125
125
  daemonHome: local.daemonHome,
126
126
  agentsRoot: paths.join(local.root, "agents-" + layout.environment + "-" + layout.localInstanceName),
127
127
  npmPrefix: local.runtimePrefix,
128
- releaseTag: environment === "dev" ? "beta" : "latest",
128
+ releaseTag: input.requestedReleaseTag ?? (environment === "dev" ? "beta" : "latest"),
129
129
  });
130
130
  }
131
131
  export function resolveAutomaticInstallTarget(input) {
@@ -133,9 +133,8 @@ export function resolveAutomaticInstallTarget(input) {
133
133
  const hostname = parsed.hostname.toLowerCase();
134
134
  const paths = input.platform === "win32" ? win32 : posix;
135
135
  const known = KNOWN_TARGETS[hostname];
136
- const targetReleaseTag = known?.environment === "dev"
137
- ? "beta"
138
- : input.requestedReleaseTag ?? "latest";
136
+ const targetReleaseTag = input.requestedReleaseTag
137
+ ?? (known?.environment === "dev" ? "beta" : "latest");
139
138
  const registryUrl = input.requestedRegistryUrl === undefined
140
139
  ? undefined
141
140
  : normalizeDaemonNpmRegistry(input.requestedRegistryUrl, { allowLoopbackHttp: targetReleaseTag === "beta" });
@@ -0,0 +1,50 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir, hostname, platform } from "node:os";
3
+ import { join } from "node:path";
4
+ const ALIYUN_MARKER = /alibaba|aliyun|alicloud/iu;
5
+ function readMarker(path) {
6
+ try {
7
+ return readFileSync(path, "utf8");
8
+ }
9
+ catch {
10
+ return "";
11
+ }
12
+ }
13
+ export function detectCloudProvider(env = process.env, platformName = platform(), hostName = hostname(), readMarkerValue = readMarker) {
14
+ const explicit = env.CREW_CLOUD_PROVIDER?.trim().toLowerCase();
15
+ if (explicit === "aliyun" || explicit === "alibaba" || explicit === "alicloud")
16
+ return "aliyun";
17
+ if (explicit !== undefined && explicit !== "")
18
+ return "unknown";
19
+ if (ALIYUN_MARKER.test(hostName))
20
+ return "aliyun";
21
+ if (platformName !== "linux")
22
+ return "unknown";
23
+ const markers = [
24
+ "/sys/class/dmi/id/sys_vendor",
25
+ "/sys/class/dmi/id/product_name",
26
+ "/sys/class/dmi/id/board_vendor",
27
+ ];
28
+ return markers.some((path) => ALIYUN_MARKER.test(readMarkerValue(path))) ? "aliyun" : "unknown";
29
+ }
30
+ export function findOssUploadScript(env = process.env, exists = existsSync) {
31
+ const explicit = env.CREW_OSS_UPLOAD_SCRIPT?.trim();
32
+ if (explicit)
33
+ return exists(explicit) ? explicit : undefined;
34
+ const home = env.HOME?.trim() || homedir();
35
+ const candidates = [
36
+ join(home, ".claude", "skills", "oss-upload", "scripts", "oss_upload.py"),
37
+ join(home, ".codex", "skills", "oss-upload", "scripts", "oss_upload.py"),
38
+ join(home, ".agents", "skills", "oss-upload", "scripts", "oss_upload.py"),
39
+ ];
40
+ return candidates.find(exists);
41
+ }
42
+ export function cloudReportUploadFacts(env = process.env, platformName = platform(), hostName = hostname(), readMarkerValue, exists) {
43
+ const provider = detectCloudProvider(env, platformName, hostName, readMarkerValue);
44
+ const ossUploadScript = provider === "aliyun" ? findOssUploadScript(env, exists) : undefined;
45
+ return Object.freeze({
46
+ provider,
47
+ ...(ossUploadScript === undefined ? {} : { ossUploadScript }),
48
+ enabled: provider === "aliyun" && ossUploadScript !== undefined,
49
+ });
50
+ }
@@ -0,0 +1,205 @@
1
+ import { execFile } from "node:child_process";
2
+ import { lstat, readdir } from "node:fs/promises";
3
+ import { extname, join, relative, resolve, sep } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { cloudReportUploadFacts } from "./cloud-report-upload.js";
6
+ import { dslog } from "./slog.js";
7
+ const execFileP = promisify(execFile);
8
+ const REPORT_FILENAMES = new Set(["report.md", "result.json"]);
9
+ const MAX_DEPTH = 8;
10
+ const MAX_FILES = 100;
11
+ const OSS_URL_HOST = "ai-claw-in.nowcoder.com";
12
+ const fileFingerprint = async (path) => {
13
+ const stat = await lstat(path);
14
+ return `${stat.size}:${stat.mtimeMs}`;
15
+ };
16
+ function isReportFile(root, path) {
17
+ const parts = relative(root, path).split(sep).filter(Boolean);
18
+ const filename = parts.at(-1) ?? "";
19
+ if (parts[0] === "tasks" && parts.length >= 3) {
20
+ return REPORT_FILENAMES.has(filename) || extname(filename).toLowerCase() === ".html";
21
+ }
22
+ if (parts[0] !== "qa-report-server-v2")
23
+ return false;
24
+ if (parts[1] === "static" && parts.length === 3)
25
+ return extname(filename).toLowerCase() === ".html";
26
+ if (parts[1] === "qa-results" && parts.length >= 4) {
27
+ return REPORT_FILENAMES.has(filename) || extname(filename).toLowerCase() === ".html";
28
+ }
29
+ return false;
30
+ }
31
+ async function walkReports(root, directory, depth, output) {
32
+ if (depth > MAX_DEPTH || output.length >= MAX_FILES)
33
+ return;
34
+ const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
35
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
36
+ if (output.length >= MAX_FILES)
37
+ return;
38
+ if (entry.name === ".git" || entry.name === "node_modules" || entry.name === ".home")
39
+ continue;
40
+ const path = join(directory, entry.name);
41
+ if (entry.isDirectory()) {
42
+ await walkReports(root, path, depth + 1, output);
43
+ }
44
+ else if (entry.isFile() && isReportFile(root, path)) {
45
+ output.push(resolve(path));
46
+ }
47
+ }
48
+ }
49
+ export async function snapshotReportFiles(root) {
50
+ const files = [];
51
+ const workspaceRoot = resolve(root);
52
+ for (const directory of [
53
+ join(workspaceRoot, "tasks"),
54
+ join(workspaceRoot, "qa-report-server-v2", "qa-results"),
55
+ join(workspaceRoot, "qa-report-server-v2", "static"),
56
+ ]) {
57
+ await walkReports(workspaceRoot, directory, 0, files);
58
+ }
59
+ const entries = await Promise.all(files.map(async (path) => [path, await fileFingerprint(path)]));
60
+ return new Map(entries);
61
+ }
62
+ export async function discoverReportFiles(root, before) {
63
+ const after = await snapshotReportFiles(root);
64
+ return [...after.entries()]
65
+ .filter(([path, fingerprint]) => before.get(path) !== fingerprint)
66
+ .map(([path]) => path)
67
+ .sort((left, right) => left.localeCompare(right));
68
+ }
69
+ function validOssUrl(value) {
70
+ if (typeof value !== "string")
71
+ return false;
72
+ try {
73
+ const url = new URL(value);
74
+ return url.protocol === "https:"
75
+ && url.hostname === OSS_URL_HOST
76
+ && !url.username && !url.password && !url.search && !url.hash
77
+ && url.pathname.length > 1;
78
+ }
79
+ catch {
80
+ return false;
81
+ }
82
+ }
83
+ async function verifyOssUrl(url) {
84
+ try {
85
+ const head = await fetch(url, { method: "HEAD", redirect: "error" });
86
+ if (head.ok)
87
+ return true;
88
+ const ranged = await fetch(url, {
89
+ method: "GET",
90
+ headers: { range: "bytes=0-0" },
91
+ redirect: "error",
92
+ });
93
+ return ranged.ok;
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
99
+ export async function uploadCloudReports(input) {
100
+ const files = await discoverReportFiles(input.root, input.before);
101
+ if (files.length === 0)
102
+ return { urls: [], failures: [] };
103
+ const run = input.execFile ?? (async (file, args) => {
104
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => (key === "PATH" || key === "HOME" || key.startsWith("CLAUDE_OSS_"))));
105
+ const result = await execFileP(file, [...args], {
106
+ env, maxBuffer: 2 * 1024 * 1024, timeout: 300_000,
107
+ });
108
+ return { stdout: result.stdout, stderr: result.stderr };
109
+ });
110
+ const result = await run("python3", [input.script, ...files, "--prefix", "nowcrew/reports", "--json"]);
111
+ let records;
112
+ try {
113
+ records = JSON.parse(result.stdout);
114
+ }
115
+ catch {
116
+ throw new Error("cloud_report_upload_invalid_json");
117
+ }
118
+ if (!Array.isArray(records))
119
+ throw new Error("cloud_report_upload_invalid_result");
120
+ const urls = [];
121
+ const failures = [];
122
+ const verify = input.verifyUrl ?? verifyOssUrl;
123
+ for (const file of files) {
124
+ const record = records.find((candidate) => (candidate && typeof candidate === "object" && "file" in candidate && candidate.file === file));
125
+ const url = record && typeof record === "object" && "url" in record ? record.url : undefined;
126
+ const success = record && typeof record === "object" && "success" in record && record.success === true;
127
+ if (success && validOssUrl(url) && await verify(url))
128
+ urls.push(url);
129
+ else
130
+ failures.push(file);
131
+ }
132
+ return { urls, failures };
133
+ }
134
+ export async function prepareCloudReportSnapshot(facts, root) {
135
+ if (!facts.enabled)
136
+ return null;
137
+ return snapshotReportFiles(root);
138
+ }
139
+ export async function prepareCloudReportExecution(input) {
140
+ const facts = cloudReportUploadFacts(input.env);
141
+ if (facts.provider !== "aliyun")
142
+ return { facts, snapshot: null };
143
+ if (facts.ossUploadScript === undefined) {
144
+ dslog("cloud_report_upload.unavailable", "阿里云机器未找到 OSS 上传脚本", {
145
+ level: "WARN", execution_id: input.executionId, agent_handle: input.agentHandle,
146
+ });
147
+ return { facts, snapshot: null };
148
+ }
149
+ try {
150
+ return { facts, snapshot: await prepareCloudReportSnapshot(facts, input.root) };
151
+ }
152
+ catch (error) {
153
+ dslog("cloud_report_upload.snapshot_failed", "报告文件快照失败", {
154
+ level: "WARN",
155
+ execution_id: input.executionId,
156
+ agent_handle: input.agentHandle,
157
+ error_message: error instanceof Error ? error.message.slice(0, 500) : String(error).slice(0, 500),
158
+ });
159
+ return { facts, snapshot: null };
160
+ }
161
+ }
162
+ export function appendReportLinks(value, urls) {
163
+ const unique = [...new Set(urls)].filter(Boolean);
164
+ if (unique.length === 0)
165
+ return value;
166
+ const suffix = `\n\n报告链接:\n${unique.map((url) => `- ${url}`).join("\n")}`;
167
+ const closeAt = value.lastIndexOf("</nowwork_external_answer>");
168
+ return closeAt >= 0
169
+ ? `${value.slice(0, closeAt)}${suffix}${value.slice(closeAt)}`
170
+ : `${value}${suffix}`;
171
+ }
172
+ export async function deliverCloudReports(input) {
173
+ if (!input.facts.enabled || input.facts.ossUploadScript === undefined || input.before === null) {
174
+ return { finalText: input.finalText, externalAnswer: input.externalAnswer };
175
+ }
176
+ try {
177
+ const result = await (input.upload ?? uploadCloudReports)({
178
+ script: input.facts.ossUploadScript,
179
+ root: input.root,
180
+ before: input.before,
181
+ });
182
+ if (result.failures.length > 0) {
183
+ dslog("cloud_report_upload.partial_failure", "部分报告文件上传失败", {
184
+ level: "WARN", execution_id: input.executionId, agent_handle: input.agentHandle,
185
+ uploaded_count: result.urls.length, failed_count: result.failures.length,
186
+ });
187
+ }
188
+ return {
189
+ finalText: input.captureFinal && result.urls.length > 0
190
+ ? appendReportLinks(input.finalText ?? "", result.urls)
191
+ : input.finalText,
192
+ externalAnswer: input.captureFinal && result.urls.length > 0
193
+ ? appendReportLinks(input.externalAnswer ?? "", result.urls)
194
+ : input.externalAnswer,
195
+ result,
196
+ };
197
+ }
198
+ catch (error) {
199
+ const message = error instanceof Error ? error.message.slice(0, 500) : String(error).slice(0, 500);
200
+ dslog("cloud_report_upload.failed", "报告上传失败", {
201
+ level: "WARN", execution_id: input.executionId, agent_handle: input.agentHandle, error_message: message,
202
+ });
203
+ return { finalText: input.finalText, externalAnswer: input.externalAnswer };
204
+ }
205
+ }
@@ -11,6 +11,7 @@ import { materializePrivateCodexHome } from "./codex-home-launch.js";
11
11
  import { spawnKimi, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
12
12
  import { applyDeepSeekHarnessMachineEnv, applyProviderEnv, providerFingerprint, } from "./provider-env.js";
13
13
  import { augmentedPath } from "./runtime-path.js";
14
+ import { deliverCloudReports, prepareCloudReportExecution } from "./cloud-report-uploader.js";
14
15
  import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./normalize.js";
15
16
  import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
16
17
  import { providerErrorFromExit } from "./provider-error.js";
@@ -252,8 +253,10 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
252
253
  let executionWorkspace = workspace;
253
254
  let localMemoryTelemetry = null;
254
255
  const inheritedEnv = { ...process.env };
256
+ const cloudReportPreparation = await prepareCloudReportExecution({ env: inheritedEnv, root: workspace.dir, executionId: input.executionId, agentHandle: input.handle });
257
+ const { facts: cloudReportUpload, snapshot: reportSnapshot } = cloudReportPreparation;
255
258
  for (const key of Object.keys(inheritedEnv)) {
256
- if (key.startsWith("CREW_AGENT_MEMORY_"))
259
+ if (key.startsWith("CREW_AGENT_MEMORY_") || ["CREW_CLOUD_PROVIDER", "CREW_OSS_UPLOAD_SCRIPT", "CREW_OSS_REPORT_UPLOAD"].includes(key))
257
260
  delete inheritedEnv[key];
258
261
  }
259
262
  const sourceCodexHome = inheritedEnv.CODEX_HOME
@@ -510,6 +513,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
510
513
  inherited_qa_runtime_root_present: inheritedEnv.QA_RUNTIME_ROOT !== undefined,
511
514
  ...packageManagerEnvFacts(childEnv),
512
515
  training_root: join(crewHome, "training"),
516
+ cloud_provider: cloudReportUpload.provider,
517
+ oss_report_upload_enabled: cloudReportUpload.enabled,
513
518
  project_context_present: input.projectContext !== undefined,
514
519
  project_ids: JSON.stringify(input.projectContext === undefined
515
520
  ? []
@@ -983,6 +988,16 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
983
988
  await writeFile(legacyCwdMarker, "1", "utf8");
984
989
  }
985
990
  }
991
+ const cloudReportResult = exitCode === 0
992
+ ? await deliverCloudReports({
993
+ facts: cloudReportUpload, root: workspace.dir, before: reportSnapshot, finalText,
994
+ externalAnswer: externalOutput.lastClosedBlock
995
+ ?? (finalText === null ? null : extractExternalAnswer(finalText)),
996
+ captureFinal: input.captureFinal, executionId: input.executionId, agentHandle: input.handle,
997
+ ...(dependencies.uploadCloudReports === undefined ? {} : { upload: dependencies.uploadCloudReports }),
998
+ })
999
+ : { finalText, externalAnswer: externalOutput.lastClosedBlock
1000
+ ?? (finalText === null ? null : extractExternalAnswer(finalText)) };
986
1001
  memoryPruneExecutorCompleted = true;
987
1002
  localMemoryTelemetry?.markExecutorCompleted();
988
1003
  return {
@@ -997,18 +1012,10 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
997
1012
  sessionId,
998
1013
  errorMessage: errorTail || null,
999
1014
  ...(providerError === undefined ? {} : { providerError }),
1000
- finalText: input.captureFinal && finalText !== null
1001
- ? stripExternalAnswerMarkers(finalText)
1002
- : null,
1003
- // 契约答案 = 整轮最后一个闭合的标签块(六个 runtime 语义一致的"后到者胜",半截块与
1004
- // 叙述里的开标签不算数);从未闭合过时才回落到对 finalText 的宽容提取,保住"收尾被
1005
- // 截断"这一老兜底。注意这放宽了"externalAnswer 非空
1006
- // ⇒ finalText 非空"的旧不变式;是否上报仍由 execution-runner 按 exitCode===0 把关。
1007
- externalAnswer: input.captureFinal
1008
- ? (externalOutput.lastClosedBlock !== null
1009
- ? externalOutput.lastClosedBlock
1010
- : finalText !== null ? extractExternalAnswer(finalText) : null)
1015
+ finalText: input.captureFinal && cloudReportResult.finalText !== null
1016
+ ? stripExternalAnswerMarkers(cloudReportResult.finalText)
1011
1017
  : null,
1018
+ externalAnswer: input.captureFinal ? cloudReportResult.externalAnswer : null,
1012
1019
  sentViaCrew,
1013
1020
  };
1014
1021
  }
@@ -95,7 +95,7 @@ export function createStorageWorkerServiceLifecycle(operation, injected = {}) {
95
95
  const targetRegistry = { userHome: operation.service.userHome, localHome: targetLocalHome };
96
96
  const sourceRecord = operation.service.sourceRecord;
97
97
  const targetRecord = operation.service.targetRecord;
98
- const sourceSpec = serviceSpec(operation, sourceRecord, operation.migration.sourceRoots["daemon-home"], "source");
98
+ const sourceSpec = serviceSpec(operation, sourceRecord, operation.migration.sourceRoots["daemon-home"]);
99
99
  const targetSpec = serviceSpec(operation, targetRecord, operation.installation.configRoot);
100
100
  assertRecordMatchesSpec(sourceRecord, sourceSpec);
101
101
  assertRecordMatchesSpec(targetRecord, targetSpec);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.86",
3
+ "version": "0.6.87-beta.2",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",